// ==UserScript== // @name 彬煤智慧课堂全自动积分 // @namespace http://tampermonkey.net/ // @version 2.1.1 // @description 自动完成每日积分,并在总控面板提供月度安排考试检测与答题(需卡密激活) // @match http://61.185.41.209:8888/PersonWap/Index* // @match http://61.185.41.209:8888/P_ExamDetail/OnlineStuday* // @match http://61.185.41.209:8888/P_ExamDetail/OnlineTest* // @grant none // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; /* * 脚本架构说明 * * 因为油猴脚本跨页面跳转会丢失上下文,用 localStorage 记录任务进度。 * 每到一个页面,检查 localStorage 里的 step,执行对应任务。 * * step 含义: * 0 = 确认登录签到(首页) * 1 = 读文章(首页) * 2 = 练习题(进入练习页 → 答题 → 回首页) * 3 = 手机考试(进入考试页 → 答题 → 回首页) * 4 = 模拟考试(反复考试到当天24分 → 回首页) * 6 = 显示结果,7 = 结果已显示 * * 页面类型(通过 URL 判断): * IndexXXXX → 首页(Index0018、Index0012等,动态获取) * OnlineStuday → 练习页 * OnlineTestOne → 考试页(手机/模拟) */ // ========== 工具函数 ========== var STATE_KEY = 'bmzhkt_auto_state'; // 状态存储键名 var VARS_KEY = 'bmzhkt_auto_vars'; // 变量存储键名 var RUN_KEY = 'bmzhkt_auto_running'; // 用户点击开始后,本轮跨页面续跑 var MONTHLY_EXAM_KEY = 'bmzhkt_monthly_exam_state'; var PAUSE_KEY = 'bmzhkt_auto_pause'; var LOCK_KEY = 'bmzhkt_auto_lock'; // 同一账号只允许一个标签页执行 var TAB_KEY = 'bmzhkt_auto_tab_id'; var LOCK_TTL = 15000; var lockHeartbeatTimer = null; var DAILY_SCORE_LIMITS = { signInScore: 1, articleScore: 15, practiceScore: 15, mobileExamScore: 24, simulateScore: 24, videoScore: 60 }; // 日志:同时输出到 console 和页面控制台面板 function log(msg) { console.log('[彬煤自动积分] ' + msg); showTip(msg); } // ========== 调试日志(跨页面保留,可一键导出)========== // 记录每一步的服务器响应和关键状态,跑完后点"📋日志"按钮一键复制 var DBG_KEY = 'bmzhkt_debug_log'; function dbg(msg) { var time = new Date().toLocaleTimeString('zh-CN', { hour12: false }); var line = '[' + time + '] ' + msg; console.log('[彬煤调试] ' + line); try { var arr = JSON.parse(localStorage.getItem(DBG_KEY) || '[]'); arr.push(line); if (arr.length > 500) arr = arr.slice(-500); localStorage.setItem(DBG_KEY, JSON.stringify(arr)); } catch (e) { } } function sanitizeDebugText(text) { var s = String(text === null || text === undefined ? '' : text); s = s.replace(/((?:pid|personid|ksmxid|examdetailid|token|session|password)\s*[=:]\s*)[^\s&,}\]]+/gi, '$1***'); s = s.replace(/\b[A-Za-z0-9_-]{16,}\b/g, '***'); return s; } // 脱敏并截断服务器响应文本用于调试(避免泄露标识或日志过长) function dbgResp(text, maxLen) { var s = sanitizeDebugText(text); var n = maxLen || 120; if (s.length > n) s = s.substring(0, n) + '…(' + s.length + '字)'; return s; } // 内存镜像只用于兼容现有页面判断;真实暂停状态按账号持久化保存。 var bmPaused = false; var activeWriteRequests = {}; // ========== 可拖拽缩放的控制台面板 ========== var tipDiv = null; // 外层容器 var taskPlanDiv = null; // 服务器积分同步后的今日任务清单 var tipBody = null; // 日志内容区 var tipLogs = []; // 日志历史(最多 100 条) var tipExpanded = true; // 是否展开(false=只显示标题栏) var tipLarge = false; // 是否大尺寸 var tipVisible = true; // 是否可见 function createTipPanel() { tipDiv = document.createElement('div'); tipDiv.id = 'bmzhkt_tip'; tipDiv.style.cssText = 'position:fixed;top:10px;right:10px;z-index:999999;' + 'background:rgba(0,0,0,0.88);color:#fff;border-radius:8px;' + 'box-shadow:0 4px 16px rgba(0,0,0,0.4);font-size:14px;' + 'line-height:1.5;overflow:hidden;user-select:none;' + 'width:300px;max-width:90vw;'; // 标题栏(可拖拽) var header = document.createElement('div'); header.style.cssText = 'display:flex;align-items:center;gap:6px;' + 'padding:6px 10px;background:rgba(255,255,255,0.1);' + 'cursor:move;font-weight:bold;border-radius:8px 8px 0 0;'; header.innerHTML = '彬煤自动积分' + '▶开始' + '开始月考' + '⏸暂停' + '📋日志' + '' + '±' + '×'; tipDiv.appendChild(header); taskPlanDiv = document.createElement('div'); taskPlanDiv.id = 'bmzhkt_task_plan'; taskPlanDiv.style.cssText = 'display:none;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.15);' + 'background:rgba(255,255,255,0.06);font-size:13px;line-height:1.6;'; tipDiv.appendChild(taskPlanDiv); // 日志内容区 tipBody = document.createElement('div'); tipBody.style.cssText = 'padding:8px 12px;max-height:300px;overflow-y:auto;' + 'word-wrap:break-word;'; tipDiv.appendChild(tipBody); document.body.appendChild(tipDiv); // 按钮事件 document.getElementById('bmzhkt_min').onclick = function (e) { e.stopPropagation(); tipExpanded = !tipExpanded; taskPlanDiv.style.display = tipExpanded && taskPlanDiv.innerHTML ? 'block' : 'none'; tipBody.style.display = tipExpanded ? 'block' : 'none'; document.getElementById('bmzhkt_min').textContent = tipExpanded ? '—' : '+'; }; document.getElementById('bmzhkt_size').onclick = function (e) { e.stopPropagation(); tipLarge = !tipLarge; tipDiv.style.width = tipLarge ? '520px' : '300px'; tipBody.style.maxHeight = tipLarge ? '500px' : '300px'; }; document.getElementById('bmzhkt_close').onclick = function (e) { e.stopPropagation(); tipVisible = false; tipDiv.style.display = 'none'; }; // 开始按钮:已完成则清除当天进度并重新跑;未完成则直接启动 var startBtn = document.getElementById('bmzhkt_start'); startBtn.onclick = function (e) { e.stopPropagation(); var accountId = getCurrentAccountId(); if (!accountId) { log('尚未取得当前账号信息,请回到已登录的智慧课堂首页后再点“开始”。'); return; } if (isPauseRequested(accountId)) { log('当前任务已暂停,请点击“继续”恢复,不要重复点击“开始”。'); return; } if (isRunActive(accountId) && !bmPaused) { log('任务正在运行,请勿重复点击“开始”。'); return; } // 防连点冷却(3 秒) if (startBtn.getAttribute('data-cooling') === '1') return; startBtn.setAttribute('data-cooling', '1'); startBtn.style.opacity = '0.5'; setTimeout(function () { startBtn.removeAttribute('data-cooling'); startBtn.style.opacity = '1'; }, 3000); var st = getState(accountId); if (!startLockHeartbeat(accountId)) { log('另一个标签页正在执行当前账号,请关闭或停止另一个标签页后再试。'); return; } if (getWorkflowSummary(st).completed) { // 今天已完成,需清除进度后从头开始 if (!confirm('今天任务已完成。\n点"确定"会清除今天的进度记录并返回首页重新开始。\n(已经拿到的积分不受影响)')) { log('已取消重新开始'); releaseExecutionLock(accountId); return; } clearState(); setRunActive(true, accountId); log('已清除当天进度,正在返回首页重新开始...'); try { location.href = getIndexUrl(); } catch (err) { log('跳转失败,尝试直接启动'); start(); } return; } // 未完成:直接继续启动 try { localStorage.removeItem(DBG_KEY); } catch (e) { } setRunActive(true, accountId); savePauseControl(transitionPauseControl(getPauseControl(accountId), 'resume-success', { now: Date.now() })); bmPaused = false; dbg('=== 开始新一轮任务 ==='); var startAction = getManualStartAction(window.location.pathname); if (startAction === 'return-index') { log('手动启动任务:先返回首页读取服务器今日积分并生成任务清单...'); try { location.href = getIndexUrl(); } catch (err) { setRunActive(false, accountId); releaseExecutionLock(accountId); log('返回首页失败,本轮没有继续执行。'); } return; } log('手动启动任务:正在读取服务器今日积分...'); start(); }; // 暂停/继续按钮:暂停覆盖每日任务和月考,继续前必须先重新取得执行锁。 var pauseBtn = document.getElementById('bmzhkt_pause'); pauseBtn.onclick = function (e) { e.stopPropagation(); var accountId = getCurrentAccountId(); var monthlyExamState = getMonthlyExamState(accountId); accountId = accountId || monthlyExamState.accountId; var pauseControl = getPauseControl(accountId); var dailyActive = isRunActive(accountId); if (!dailyActive && !monthlyExamState.active && pauseControl.status === 'running') { log('任务尚未启动,请先点击“开始”按钮。'); return; } if (pauseControl.status === 'pausing') { log('正在等待已发出的请求结束,结束后按钮会变为“继续”。'); return; } if (pauseControl.status === 'paused') { if (!startLockHeartbeat(accountId)) { pauseControl = transitionPauseControl(pauseControl, 'resume-failed', { now: Date.now() }); savePauseControl(pauseControl); renderPauseButton(accountId); log('另一个标签页正在执行当前账号,无法继续。'); return; } pauseControl = transitionPauseControl(pauseControl, 'resume-success', { now: Date.now() }); savePauseControl(pauseControl); bmPaused = false; if (pauseControl.flow !== 'monthly') setRunActive(true, accountId); renderPauseButton(accountId); log('已继续,正在重新核对当前进度...'); if (pauseControl.flow !== 'monthly' && String(window.location.pathname || '').indexOf('/PersonWap/Index') < 0) { try { location.href = getIndexUrl(); } catch (err) { log('返回首页失败,请手动回到智慧课堂首页后再点“继续”。'); } return; } start(); } else { var flow = monthlyExamState.active ? 'monthly' : 'daily'; pauseControl = transitionPauseControl(pauseControl, 'pause', { activeWrites: getActiveWriteCount(accountId), flow: flow, now: Date.now() }); savePauseControl(pauseControl); bmPaused = true; if (flow === 'daily') setRunActive(false, accountId); if (pauseControl.status === 'paused') releaseExecutionLock(accountId); renderPauseButton(accountId); log(pauseControl.status === 'pausing' ? '正在安全暂停:等待已发出的请求返回,期间不会再发新请求。' : '已暂停。点“继续”后会重新核对进度再恢复。'); } }; // 调试日志按钮:弹出浮窗显示全部调试日志,可一键复制 var logBtn = document.getElementById('bmzhkt_log'); logBtn.onclick = function (e) { e.stopPropagation(); showDebugLog(); }; document.getElementById('bmzhkt_monthly_action').onclick = function (e) { e.stopPropagation(); handleMonthlyExamPanelAction(); }; renderMonthlyExamPanel(getMonthlyExamState(getCurrentAccountId())); renderPauseButton(getCurrentAccountId()); // 拖拽逻辑 var dragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0; header.onmousedown = function (e) { if (e.target.id === 'bmzhkt_start' || e.target.id === 'bmzhkt_monthly_action' || e.target.id === 'bmzhkt_pause' || e.target.id === 'bmzhkt_log' || e.target.id === 'bmzhkt_min' || e.target.id === 'bmzhkt_size' || e.target.id === 'bmzhkt_close') return; dragging = true; startX = e.clientX; startY = e.clientY; var rect = tipDiv.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; // 切换为 left/top 定位以便自由移动 tipDiv.style.left = startLeft + 'px'; tipDiv.style.top = startTop + 'px'; tipDiv.style.right = 'auto'; e.preventDefault(); }; document.addEventListener('mousemove', function (e) { if (!dragging) return; var nx = startLeft + (e.clientX - startX); var ny = startTop + (e.clientY - startY); // 限制不超出视口 nx = Math.max(0, Math.min(window.innerWidth - 60, nx)); ny = Math.max(0, Math.min(window.innerHeight - 30, ny)); tipDiv.style.left = nx + 'px'; tipDiv.style.top = ny + 'px'; }); document.addEventListener('mouseup', function () { dragging = false; }); // 触摸拖拽支持 header.ontouchstart = function (e) { if (e.target.id === 'bmzhkt_start' || e.target.id === 'bmzhkt_monthly_action' || e.target.id === 'bmzhkt_pause' || e.target.id === 'bmzhkt_log' || e.target.id === 'bmzhkt_min' || e.target.id === 'bmzhkt_size' || e.target.id === 'bmzhkt_close') return; var touch = e.touches[0]; dragging = true; startX = touch.clientX; startY = touch.clientY; var rect = tipDiv.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; tipDiv.style.left = startLeft + 'px'; tipDiv.style.top = startTop + 'px'; tipDiv.style.right = 'auto'; e.preventDefault(); }; document.addEventListener('touchmove', function (e) { if (!dragging) return; var touch = e.touches[0]; var nx = startLeft + (touch.clientX - startX); var ny = startTop + (touch.clientY - startY); nx = Math.max(0, Math.min(window.innerWidth - 60, nx)); ny = Math.max(0, Math.min(window.innerHeight - 30, ny)); tipDiv.style.left = nx + 'px'; tipDiv.style.top = ny + 'px'; e.preventDefault(); }, false); document.addEventListener('touchend', function () { dragging = false; }); } function showTip(msg) { if (!document.body) { setTimeout(function () { showTip(msg); }, 200); return; } if (!tipDiv) { createTipPanel(); } // 关闭后收到新日志则重新显示 if (!tipVisible) { tipVisible = true; tipDiv.style.display = 'block'; } var time = new Date().toLocaleTimeString('zh-CN', { hour12: false }); var entry = document.createElement('div'); entry.style.cssText = 'margin-bottom:3px;border-bottom:1px solid rgba(255,255,255,0.08);padding-bottom:3px;'; entry.innerHTML = '' + time + ' ' + '' + escapeHtml(msg) + ''; tipBody.appendChild(entry); tipLogs.push(entry); // 超过 100 条删掉最早的 while (tipLogs.length > 100) { var old = tipLogs.shift(); if (old && old.parentNode) old.parentNode.removeChild(old); } // 自动滚动到底部 tipBody.scrollTop = tipBody.scrollHeight; } function renderDailyTaskPlan(state) { if (!document.body) return; if (!tipDiv) createTipPanel(); var plan = getDailyTaskPlan(state); var html = '
今日任务 ' + plan.total + '/' + plan.targetTotal + ' 分
'; for (var i = 0; i < plan.items.length; i++) { var item = plan.items[i]; var status = item.completed ? '已完成' : '待补 ' + item.remaining + ' 分'; var color = item.completed ? '#81c784' : '#ffcc80'; html += '
' + '' + escapeHtml(item.label) + ' ' + item.current + '/' + item.limit + '' + '' + status + '
'; } if (plan.pendingTaskIds.length > 0) { html += '
执行顺序:' + escapeHtml(plan.pendingLabels.join(' → ')) + '
'; } else { html += '
五项均已达到今日上限
'; } taskPlanDiv.innerHTML = html; taskPlanDiv.style.display = tipExpanded ? 'block' : 'none'; } function renderMonthlyExamPanel(state) { if (!document.body) return; if (!tipDiv) createTipPanel(); var source = state || createDefaultMonthlyExamState(getCurrentAccountId()); var button = document.getElementById('bmzhkt_monthly_action'); if (!button) return; var actionLabel = '开始月考'; var hint = '点击后检测当前开放的安排考试,并自动答题交卷'; if (source.active && source.phase === 'detecting') { actionLabel = '检测中'; hint = '正在打开安排的考试列表'; } else if (source.active && source.phase === 'running') { actionLabel = '月考中'; hint = '正在答题并等待服务器交卷结果'; } else if (source.status === 'available') { hint = '已检测到开放场次,正在自动进入试卷答题'; } else if (source.status === 'completed' || source.status === 'submitted') { hint = '本月已交卷' + (source.score !== null ? ',成绩 ' + source.score + ' 分' : ''); } else if (source.status === 'upcoming') { hint = '月考尚未开放'; } else if (source.status === 'none') { hint = '当前没有可参加的月考'; } else if (source.status === 'error') { hint = source.message || '操作未完成,请重新检测'; } button.textContent = actionLabel; button.title = hint; button.style.opacity = source.active ? '0.65' : '1'; } // 弹出调试日志浮窗:显示全部日志,可全选复制或一键复制 function showDebugLog() { var arr = []; try { arr = JSON.parse(localStorage.getItem(DBG_KEY) || '[]'); } catch (e) { } var text = arr.length > 0 ? arr.join('\n') : '(暂无调试日志,跑一遍任务后再点此按钮)'; var mask = document.createElement('div'); mask.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:999999;background:rgba(0,0,0,0.7);'; var box = document.createElement('div'); box.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.4);width:620px;max-width:92vw;max-height:80vh;display:flex;flex-direction:column;overflow:hidden;'; box.innerHTML = '
' + '调试日志(复制或下载后发给开发者)' + '📋复制' + '⬇下载' + '🗑清空' + '×' + '
' + ''; mask.appendChild(box); document.body.appendChild(mask); var ta = document.getElementById('bmzhkt_dbg_text'); ta.value = text; document.getElementById('bmzhkt_dbg_close').onclick = function () { if (mask.parentNode) mask.parentNode.removeChild(mask); }; document.getElementById('bmzhkt_dbg_copy').onclick = function () { ta.select(); try { document.execCommand('copy'); } catch (e) { } var btn = document.getElementById('bmzhkt_dbg_copy'); btn.textContent = '✓已复制'; setTimeout(function () { btn.textContent = '📋复制'; }, 1500); }; document.getElementById('bmzhkt_dbg_dl').onclick = function () { var text = ta.value; var blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = '彬煤调试日志_' + new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-') + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); var btn = document.getElementById('bmzhkt_dbg_dl'); btn.textContent = '✓已下载'; setTimeout(function () { btn.textContent = '⬇下载'; }, 1500); }; document.getElementById('bmzhkt_dbg_clear').onclick = function () { if (confirm('确定清空调试日志吗?')) { try { localStorage.removeItem(DBG_KEY); } catch (e) { } ta.value = '(已清空)'; } }; mask.onclick = function (e) { if (e.target === mask && mask.parentNode) mask.parentNode.removeChild(mask); }; } // HTML 转义,防止服务器返回值注入恶意脚本 function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // 等待条件满足(轮询),超时后回调 false function waitFor(condFn, timeout, cb) { var start = Date.now(); function check() { try { if (condFn()) { cb(true); return; } } catch (e) { /* 忽略 */ } if (Date.now() - start > timeout) { cb(false); return; } setTimeout(check, 300); } check(); } function getLocalDateKey(date) { var d = date || new Date(); var month = ('0' + (d.getMonth() + 1)).slice(-2); var day = ('0' + d.getDate()).slice(-2); return d.getFullYear() + '-' + month + '-' + day; } function normalizeAccountId(value) { return String(value === null || typeof value === 'undefined' ? '' : value).trim(); } function createDefaultState(dateKey, accountId) { return { runDate: dateKey || getLocalDateKey(), schemaVersion: 6, accountId: normalizeAccountId(accountId), step: 0, signInScore: 0, articleScore: 0, articleLastUploadAt: 0, practiceScore: 0, mobileExamScore: 0, simulateScore: 0, mobileExamAttemptIndex: 0, simulateExamAttemptIndex: 0, videoScore: 0, videoCourseIndex: 0, dailyLimits: getDailyScoreLimits(), simulateCompletedCount: 0, articleLimitReached: false, exerciseLimitReached: false, serverDailyTotal: null, pendingWrite: null, skippedTasks: [], errors: [], stepRetries: 0 }; } function normalizeSkippedTasks(value) { var source = Array.isArray(value) ? value : []; var result = []; var allowed = ['signin', 'article', 'practice', 'mobile', 'simulate', 'video']; for (var i = 0; i < source.length; i++) { var row = source[i]; var id = typeof row === 'string' ? row : String(row && row.id || ''); if (allowed.indexOf(id) < 0 || isTaskSkipped({ skippedTasks: result }, id)) continue; result.push({ id: id, reason: typeof row === 'object' && row ? String(row.reason || '未知原因').substring(0, 120) : '未知原因' }); } return result; } function isTaskSkipped(state, taskId) { var rows = state && Array.isArray(state.skippedTasks) ? state.skippedTasks : []; for (var i = 0; i < rows.length; i++) { var id = typeof rows[i] === 'string' ? rows[i] : rows[i] && rows[i].id; if (id === taskId) return true; } return false; } function markTaskSkipped(state, taskId, reason) { if (!state || !taskId) return state; if (!Array.isArray(state.skippedTasks)) state.skippedTasks = []; if (!isTaskSkipped(state, taskId)) { state.skippedTasks.push({ id: taskId, reason: String(reason || '未知原因').substring(0, 120) }); } return state; } function getTaskIdForStep(step) { return ['signin', 'article', 'practice', 'mobile', 'simulate'][Number(step)] || ''; } function getTaskLabel(taskId) { var labels = { signin: '登录签到', article: '读文章', practice: '练习题', mobile: '手机考试', simulate: '模拟考试', video: '视频学习' }; return labels[taskId] || taskId || '当前项目'; } function normalizeState(value, today, accountId) { var dateKey = today || getLocalDateKey(); var requestedAccountId = normalizeAccountId(accountId); var savedAccountId = normalizeAccountId(value && value.accountId); if (!value || typeof value !== 'object' || value.runDate !== dateKey) { return createDefaultState(dateKey, requestedAccountId); } if (requestedAccountId && !savedAccountId) { return createDefaultState(dateKey, requestedAccountId); } if (requestedAccountId && savedAccountId && requestedAccountId !== savedAccountId) { return createDefaultState(dateKey, requestedAccountId); } var sourceSchemaVersion = Math.max(0, Number(value.schemaVersion) || 0); var state = createDefaultState(dateKey, requestedAccountId || savedAccountId); var parsedStep = Number(value.step); var savedStep = isFinite(parsedStep) && parsedStep >= 0 && parsedStep <= 7 && Math.floor(parsedStep) === parsedStep ? parsedStep : 0; state.signInScore = Math.max(0, Math.min(1, Number(value.signInScore) || 0)); state.dailyLimits = normalizeDailyScoreLimits(value.dailyLimits); state.articleScore = Math.max(0, Math.min(state.dailyLimits.articleScore, Number(value.articleScore) || 0)); state.articleLastUploadAt = Math.max(0, Number(value.articleLastUploadAt) || 0); state.practiceScore = Math.max(0, Math.min(state.dailyLimits.practiceScore, Number(value.practiceScore) || 0)); state.mobileExamScore = Math.max(0, Math.min(state.dailyLimits.mobileExamScore, Number(value.mobileExamScore) || 0)); state.simulateScore = Math.max(0, Math.min(state.dailyLimits.simulateScore, Number(value.simulateScore) || 0)); state.mobileExamAttemptIndex = Math.max(0, Math.floor(Number(value.mobileExamAttemptIndex) || Math.floor(state.mobileExamScore / 4))); state.simulateExamAttemptIndex = Math.max(0, Math.floor(Number(value.simulateExamAttemptIndex) || Math.floor(state.simulateScore / 4))); state.videoScore = Math.max(0, Math.min(state.dailyLimits.videoScore, Number(value.videoScore) || 0)); state.videoCourseIndex = Math.max(0, Math.floor(Number(value.videoCourseIndex) || 0)); state.simulateCompletedCount = getSimulateCompletedCount(value); state.articleLimitReached = value.articleLimitReached === true; state.exerciseLimitReached = value.exerciseLimitReached === true; state.serverDailyTotal = value.serverDailyTotal !== null && typeof value.serverDailyTotal !== 'undefined' && isFinite(Number(value.serverDailyTotal)) && Number(value.serverDailyTotal) >= 0 ? Number(value.serverDailyTotal) : null; state.pendingWrite = normalizePendingWrite(value.pendingWrite); state.skippedTasks = normalizeSkippedTasks(value.skippedTasks); state.errors = Array.isArray(value.errors) ? value.errors.slice(0, 50).map(String) : []; state.errors = clearLegacyPassingScoreErrors(state.errors); state.errors = clearErrorsByPrefixes(state.errors, ['video:', '视频学习']); state.stepRetries = Math.max(0, Number(value.stepRetries) || 0); // 1.8.4/1.8.5 的考试入口和交卷接口会误把三场考试标为跳过。 // 首次升级到 schema 4 时只重开尚未取得积分的考试,保留其他已拿积分。 if (sourceSchemaVersion < 4) { var migratedSkippedTasks = []; for (var i = 0; i < state.skippedTasks.length; i++) { var skippedId = state.skippedTasks[i].id; var shouldRetry = (skippedId === 'mobile' && state.mobileExamScore <= 0) || ((skippedId === 'simulate1' || skippedId === 'simulate2') && state.simulateCompletedCount < 2); if (!shouldRetry) migratedSkippedTasks.push(state.skippedTasks[i]); } if (migratedSkippedTasks.length !== state.skippedTasks.length) { state.skippedTasks = migratedSkippedTasks; state.errors = clearErrorsByPrefixes(state.errors, [ '手机考试', 'mobile', '模拟考试', 'simulate' ]); state.stepRetries = 0; } } var summary = getWorkflowSummary(state); state.step = summary.completed && savedStep === 7 ? 7 : summary.nextStep; if (state.step !== savedStep) state.stepRetries = 0; return state; } function normalizeRunControl(value, today, accountId) { var dateKey = today || getLocalDateKey(); var requestedAccountId = normalizeAccountId(accountId); var savedAccountId = normalizeAccountId(value && value.accountId); if (!value || typeof value !== 'object' || value.runDate !== dateKey) { return { runDate: dateKey, active: false, accountId: requestedAccountId }; } if (requestedAccountId && savedAccountId && requestedAccountId !== savedAccountId) { return { runDate: dateKey, active: false, accountId: requestedAccountId }; } return { runDate: dateKey, active: value.active === true, accountId: requestedAccountId || savedAccountId }; } function getRunControl(accountId) { try { return normalizeRunControl(JSON.parse(localStorage.getItem(RUN_KEY) || 'null'), null, accountId); } catch (e) { return normalizeRunControl(null, null, accountId); } } function setRunActive(active, accountId) { var id = arguments.length > 1 ? normalizeAccountId(accountId) : getCurrentAccountId(); try { localStorage.setItem(RUN_KEY, JSON.stringify({ runDate: getLocalDateKey(), active: active === true, accountId: id })); } catch (e) { } } function isRunActive(accountId) { return getRunControl(accountId).active; } function normalizePauseControl(value, today, accountId) { var dateKey = today || getLocalDateKey(); var requestedAccountId = normalizeAccountId(accountId); var savedAccountId = normalizeAccountId(value && value.accountId); var running = { runDate: dateKey, accountId: requestedAccountId, status: 'running', flow: 'daily', updatedAt: 0 }; if (!value || typeof value !== 'object' || value.runDate !== dateKey) return running; if (requestedAccountId && (!savedAccountId || requestedAccountId !== savedAccountId)) return running; var status = ['running', 'pausing', 'paused'].indexOf(value.status) >= 0 ? value.status : 'running'; return { runDate: dateKey, accountId: requestedAccountId || savedAccountId, status: status, flow: value.flow === 'monthly' ? 'monthly' : 'daily', updatedAt: Math.max(0, Number(value.updatedAt) || 0) }; } function transitionPauseControl(control, eventName, context) { var current = normalizePauseControl(control, control && control.runDate, control && control.accountId); var next = { runDate: current.runDate, accountId: current.accountId, status: current.status, flow: current.flow, updatedAt: Math.max(0, Number(context && context.now) || Date.now()) }; if (eventName === 'pause') { next.status = Number(context && context.activeWrites) > 0 ? 'pausing' : 'paused'; next.flow = context && context.flow === 'monthly' ? 'monthly' : 'daily'; } else if (eventName === 'write-finished' && current.status === 'pausing' && Number(context && context.activeWrites) <= 0) { next.status = 'paused'; } else if (eventName === 'resume-success') { next.status = 'running'; } return next; } function getPauseControl(accountId) { try { return normalizePauseControl(JSON.parse(localStorage.getItem(PAUSE_KEY) || 'null'), null, accountId); } catch (e) { return normalizePauseControl(null, null, accountId); } } function savePauseControl(control) { try { localStorage.setItem(PAUSE_KEY, JSON.stringify(control)); } catch (e) { } } function isPauseRequested(accountId) { return getPauseControl(accountId).status !== 'running'; } function renderPauseButton(accountId) { var button = typeof document !== 'undefined' ? document.getElementById('bmzhkt_pause') : null; if (!button) return; var status = getPauseControl(accountId).status; if (status === 'pausing') { button.textContent = '暂停中'; button.style.background = '#757575'; } else if (status === 'paused') { button.textContent = '▶继续'; button.style.background = '#388e3c'; } else { button.textContent = '⏸暂停'; button.style.background = '#f57c00'; } } function getActiveWriteCount(accountId) { return Math.max(0, Number(activeWriteRequests[normalizeAccountId(accountId)] || 0)); } function beginTrackedWrite(accountId) { var id = normalizeAccountId(accountId); if (!id) return; activeWriteRequests[id] = getActiveWriteCount(id) + 1; } function finishTrackedWrite(accountId) { var id = normalizeAccountId(accountId); if (!id) return; activeWriteRequests[id] = Math.max(0, getActiveWriteCount(id) - 1); var pauseControl = getPauseControl(id); if (pauseControl.status === 'pausing' && getActiveWriteCount(id) === 0) { pauseControl = transitionPauseControl(pauseControl, 'write-finished', { activeWrites: 0, now: Date.now() }); savePauseControl(pauseControl); bmPaused = true; releaseExecutionLock(id); renderPauseButton(id); log('已安全暂停。点“继续”后会重新核对进度再恢复。'); } } function createDefaultMonthlyExamState(accountId) { return { accountId: normalizeAccountId(accountId), active: false, phase: 'idle', status: 'unknown', title: '', startAt: 0, endAt: 0, selectedIndex: -1, score: null, message: '' }; } function normalizeMonthlyExamState(value, accountId) { var requestedAccountId = normalizeAccountId(accountId); var savedAccountId = normalizeAccountId(value && value.accountId); if (!value || typeof value !== 'object' || (requestedAccountId && savedAccountId && requestedAccountId !== savedAccountId)) { return createDefaultMonthlyExamState(requestedAccountId); } var state = createDefaultMonthlyExamState(requestedAccountId || savedAccountId); var allowedStatus = ['unknown', 'available', 'completed', 'submitted', 'upcoming', 'none', 'error']; var allowedPhase = ['idle', 'detecting', 'ready', 'running']; state.active = value.active === true; state.phase = allowedPhase.indexOf(value.phase) >= 0 ? value.phase : 'idle'; state.status = allowedStatus.indexOf(value.status) >= 0 ? value.status : 'unknown'; state.title = String(value.title || '').substring(0, 160); state.startAt = Math.max(0, Number(value.startAt) || 0); state.endAt = Math.max(0, Number(value.endAt) || 0); state.selectedIndex = Math.max(-1, Math.floor(Number(value.selectedIndex) || -1)); state.score = value.score !== null && isFinite(Number(value.score)) ? Number(value.score) : null; state.message = String(value.message || '').substring(0, 120); if (!state.active && (state.phase === 'detecting' || state.phase === 'running')) state.phase = 'idle'; return state; } function getMonthlyExamState(accountId) { try { return normalizeMonthlyExamState(JSON.parse(localStorage.getItem(MONTHLY_EXAM_KEY) || 'null'), accountId); } catch (e) { return createDefaultMonthlyExamState(accountId); } } function saveMonthlyExamState(state) { try { localStorage.setItem(MONTHLY_EXAM_KEY, JSON.stringify(state)); } catch (e) { } } function finishMonthlyExamAction(state, status, message) { state.active = false; state.phase = 'idle'; state.status = status; state.message = String(message || '').substring(0, 120); saveMonthlyExamState(state); if (status === 'completed' || status === 'submitted') { var pauseControl = getPauseControl(state.accountId); if (pauseControl.status !== 'running' && pauseControl.flow === 'monthly') { pauseControl = transitionPauseControl(pauseControl, 'resume-success', { now: Date.now() }); savePauseControl(pauseControl); bmPaused = false; } } releaseExecutionLock(state.accountId); renderMonthlyExamPanel(state); renderPauseButton(state.accountId); } // 读取当天状态;跨天自动从 step=0 重新开始 function getState(accountId) { try { var s = localStorage.getItem(STATE_KEY); if (s) return normalizeState(JSON.parse(s), null, accountId); } catch (e) { } return createDefaultState(null, accountId); } // 保存状态 function saveState(state) { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } // 清除状态(重新开始) function clearState() { localStorage.removeItem(STATE_KEY); localStorage.removeItem('bmzhkt_exam_ksmxid'); } // 步骤失败时有限自动重试;用尽后停止本轮,保留当前未完成项目供下次继续。 var MAX_STEP_RETRIES = 2; function getStepFailureAction(completedRetries, maxRetries) { var retries = Math.max(0, Number(completedRetries) || 0); var limit = Math.max(0, Number(maxRetries) || 0); return retries < limit ? 'retry' : 'stop'; } function handleStepFailure(state) { if (isPauseRequested(state && state.accountId)) { log('当前已暂停,本次不会自动重试或跳页。'); return; } var maxRetries = arguments.length > 1 ? Math.max(0, Number(arguments[1]) || 0) : MAX_STEP_RETRIES; var action = getStepFailureAction(state.stepRetries, maxRetries); var failureCount = (state.stepRetries || 0) + 1; dbg('[失败] step=' + state.step + ', 第' + failureCount + '次失败, action=' + action + ', errors=' + JSON.stringify(state.errors || [])); if (action === 'retry') { state.stepRetries = failureCount; saveState(state); log('第 ' + state.stepRetries + ' 次失败,自动重试中(最多 ' + maxRetries + ' 次)...'); setTimeout(function () { if (!isPauseRequested(state.accountId) && isRunActive(state.accountId)) { location.href = getIndexUrl(); } }, 3000); return; } var taskId = getTaskIdForStep(state.step); state.stepRetries = 0; saveState(state); setRunActive(false, state.accountId); releaseExecutionLock(state.accountId); log('【' + getTaskLabel(taskId) + '】连续失败,本轮已停止且没有跳过该项目。请检查网络或页面后再次点击“开始”继续。'); dbg('[停止] task=' + taskId + ', step=' + state.step + ', 保留为未完成'); } // 保存页面全局变量到 localStorage(供跨页面使用) function saveVars() { var accountId = normalizeAccountId(typeof M_PersonId !== 'undefined' ? M_PersonId : ''); var vars = { runDate: getLocalDateKey(), accountId: accountId }; // 收集所有 M_ 开头的全局变量 var names = [ 'M_PersonId', 'M_ExamTypeId', 'M_Code', 'M_WX', 'M_StudyTimeAccumulate', 'M_StudyTimeOneMaxAccumulate', 'M_StudyTimeMaxAccumulate', 'M_MobileExercisesAccumulate', 'M_MobileExercisesOneMaxAccumulate', 'M_MobileExercisesMaxAccumulate', 'M_StudyTimeAccumulateId', 'M_MobileExercisesAccumulateId', 'M_Style', 'M_ExamTypeName', 'M_DepartmentId' ]; for (var i = 0; i < names.length; i++) { var name = names[i]; try { if (typeof window[name] !== 'undefined' && window[name] !== null && window[name] !== '') { vars[name] = window[name]; } } catch (e) { } } // 合并已有的变量(不覆盖) try { var existing = normalizeVarsCache(JSON.parse(localStorage.getItem(VARS_KEY) || '{}'), null, accountId); for (var k in existing) { if (!(k in vars)) vars[k] = existing[k]; } } catch (e) { } localStorage.setItem(VARS_KEY, JSON.stringify(vars)); return vars; } // 从 localStorage 读取变量,并设置到当前页面(如果页面没有的话) function loadVars(accountId) { try { var expectedAccountId = normalizeAccountId(accountId) || getCurrentAccountId(false); var vars = normalizeVarsCache(JSON.parse(localStorage.getItem(VARS_KEY) || '{}'), null, expectedAccountId); for (var k in vars) { if (k !== 'runDate' && k !== 'accountId' && (typeof window[k] === 'undefined' || window[k] === null || window[k] === '')) { window[k] = vars[k]; } } return vars; } catch (e) { return {}; } } function normalizeVarsCache(value, today, accountId) { var dateKey = today || getLocalDateKey(); if (!value || typeof value !== 'object' || value.runDate !== dateKey) return {}; var requestedAccountId = normalizeAccountId(accountId); var savedAccountId = normalizeAccountId(value.accountId || value.M_PersonId); if (requestedAccountId && savedAccountId && requestedAccountId !== savedAccountId) return {}; return value; } function normalizePendingWrite(value) { if (!value || typeof value !== 'object' || !value.kind) return null; return { kind: String(value.kind), taskId: String(value.taskId || ''), step: Math.max(0, Number(value.step) || 0), beforeDailyTotal: value.beforeDailyTotal !== null && typeof value.beforeDailyTotal !== 'undefined' && isFinite(Number(value.beforeDailyTotal)) ? Number(value.beforeDailyTotal) : null, beforeStudyAcc: value.beforeStudyAcc !== null && typeof value.beforeStudyAcc !== 'undefined' && isFinite(Number(value.beforeStudyAcc)) ? Number(value.beforeStudyAcc) : null, beforeExerciseAcc: value.beforeExerciseAcc !== null && typeof value.beforeExerciseAcc !== 'undefined' && isFinite(Number(value.beforeExerciseAcc)) ? Number(value.beforeExerciseAcc) : null, expectedMinPoints: Math.max(0, Number(value.expectedMinPoints) || 0), estimatedPoints: Math.max(0, Number(value.estimatedPoints) || 0), createdAt: Math.max(0, Number(value.createdAt) || 0) }; } function getCurrentAccountId(useCache) { var direct = ''; try { direct = normalizeAccountId(window.M_PersonId); } catch (e) { } if (direct || useCache === false) return direct; try { var cached = normalizeVarsCache(JSON.parse(localStorage.getItem(VARS_KEY) || '{}')); return normalizeAccountId(cached.accountId || cached.M_PersonId); } catch (e) { return ''; } } function getTabId() { var id = ''; try { id = sessionStorage.getItem(TAB_KEY) || ''; } catch (e) { } if (!id) { id = 'tab-' + Date.now() + '-' + Math.random().toString(36).slice(2); try { sessionStorage.setItem(TAB_KEY, id); } catch (e) { } } return id; } function canAcquireExecutionLock(lock, ownerId, accountId, today, now, ttl) { if (!lock || typeof lock !== 'object') return true; if (String(lock.runDate || '') !== String(today || getLocalDateKey())) return true; if (normalizeAccountId(lock.accountId) !== normalizeAccountId(accountId)) return true; if (String(lock.ownerId || '') === String(ownerId || '')) return true; return (Number(now) || Date.now()) - (Number(lock.heartbeatAt) || 0) > (Number(ttl) || LOCK_TTL); } function readExecutionLock() { try { return JSON.parse(localStorage.getItem(LOCK_KEY) || 'null'); } catch (e) { return null; } } function ownsExecutionLock(accountId) { var lock = readExecutionLock(); return !!lock && lock.runDate === getLocalDateKey() && normalizeAccountId(lock.accountId) === normalizeAccountId(accountId) && lock.ownerId === getTabId() && Date.now() - Number(lock.heartbeatAt || 0) <= LOCK_TTL; } function refreshExecutionLock(accountId) { if (!normalizeAccountId(accountId)) return false; var lock = readExecutionLock(); var ownerId = getTabId(); var now = Date.now(); if (!canAcquireExecutionLock(lock, ownerId, accountId, getLocalDateKey(), now, LOCK_TTL)) return false; try { localStorage.setItem(LOCK_KEY, JSON.stringify({ runDate: getLocalDateKey(), accountId: normalizeAccountId(accountId), ownerId: ownerId, heartbeatAt: now })); } catch (e) { return false; } return ownsExecutionLock(accountId); } function startLockHeartbeat(accountId) { if (lockHeartbeatTimer) clearInterval(lockHeartbeatTimer); if (!refreshExecutionLock(accountId)) return false; lockHeartbeatTimer = setInterval(function () { var pauseControl = getPauseControl(accountId); var monthlyActive = getMonthlyExamState(accountId).active; var keepForPendingWrite = pauseControl.status === 'pausing' && getActiveWriteCount(accountId) > 0; var flowActive = isRunActive(accountId) || monthlyActive || keepForPendingWrite; if (!flowActive || pauseControl.status === 'paused' || !refreshExecutionLock(accountId)) { clearInterval(lockHeartbeatTimer); lockHeartbeatTimer = null; } }, 5000); return true; } function releaseExecutionLock(accountId) { if (lockHeartbeatTimer) { clearInterval(lockHeartbeatTimer); lockHeartbeatTimer = null; } if (!ownsExecutionLock(accountId)) return; try { localStorage.removeItem(LOCK_KEY); } catch (e) { } } function areIndexVarsReady(code) { return !!code; } function getRequestFailureAction(error, isWrite) { return isWrite === true && String(error || '').toLowerCase() === 'timeout' ? 'reconcile' : 'retry'; } function reconcilePendingWrite(pending, snapshot) { var item = normalizePendingWrite(pending); if (!item) return 'none'; if (item.kind === 'exam-save' || item.kind === 'exam-end') return 'pending'; var current = snapshot || {}; var expected = Math.max(0, Number(item.expectedMinPoints) || 0); var dailyIncreased = item.beforeDailyTotal !== null && isFinite(Number(current.dailyTotal)) && Number(current.dailyTotal) - item.beforeDailyTotal >= expected; var studyIncreased = item.beforeStudyAcc !== null && isFinite(Number(current.studyAcc)) && Number(current.studyAcc) - item.beforeStudyAcc >= expected; var exerciseIncreased = item.beforeExerciseAcc !== null && isFinite(Number(current.exerciseAcc)) && Number(current.exerciseAcc) - item.beforeExerciseAcc >= expected; return dailyIncreased || studyIncreased || exerciseIncreased ? 'confirmed' : 'pending'; } function makePendingWrite(state, kind, estimatedPoints) { var vars = loadVars(state && state.accountId); return { kind: kind, taskId: getTaskIdForStep(state && state.step), step: Number(state && state.step) || 0, beforeDailyTotal: state && state.serverDailyTotal !== null && typeof state.serverDailyTotal !== 'undefined' && isFinite(Number(state.serverDailyTotal)) ? Number(state.serverDailyTotal) : null, beforeStudyAcc: vars.M_StudyTimeAccumulate !== null && typeof vars.M_StudyTimeAccumulate !== 'undefined' && isFinite(Number(vars.M_StudyTimeAccumulate)) ? Number(vars.M_StudyTimeAccumulate) : null, beforeExerciseAcc: vars.M_MobileExercisesAccumulate !== null && typeof vars.M_MobileExercisesAccumulate !== 'undefined' && isFinite(Number(vars.M_MobileExercisesAccumulate)) ? Number(vars.M_MobileExercisesAccumulate) : null, expectedMinPoints: Math.max(1, Number(estimatedPoints) || 1), estimatedPoints: Math.max(0, Number(estimatedPoints) || 0), createdAt: Date.now() }; } function handleAmbiguousWrite(state, pending, label) { state.pendingWrite = normalizePendingWrite(pending); state.stepRetries = 0; state.errors.push(String(label || '写入') + ': timeout awaiting reconciliation'); saveState(state); setRunActive(false, state.accountId); releaseExecutionLock(state.accountId); log('【' + String(label || '写入') + '】请求超时,服务器可能已经收到。为避免重复提交,脚本已停止,请刷新首页后再次点击“开始”核实。'); dbg('[待核实] kind=' + state.pendingWrite.kind + ', step=' + state.pendingWrite.step); } function applyConfirmedPendingWrite(state) { var pending = state.pendingWrite; if (!pending) return state; var points = Math.max(0, Number(pending.estimatedPoints) || 0); var limits = normalizeDailyScoreLimits(state.dailyLimits); if (pending.taskId === 'article') state.articleScore = Math.min(limits.articleScore, state.articleScore + points); else if (pending.taskId === 'practice') state.practiceScore = Math.min(limits.practiceScore, state.practiceScore + points); else if (pending.taskId === 'mobile') state.mobileExamScore = Math.min(limits.mobileExamScore, state.mobileExamScore + points); else if (pending.taskId === 'simulate') state.simulateScore = Math.min(limits.simulateScore, state.simulateScore + points); state.pendingWrite = null; state.errors = clearErrorsByPrefixes(state.errors, [getTaskLabel(pending.taskId) + ':', pending.taskId + ':']); state.step = getWorkflowSummary(state).nextStep; state.stepRetries = 0; return state; } // 所有任务请求直接使用原生异步 XHR,并在发出请求前启动独立定时器。 // 不使用网站的 jQuery AJAX,避免网站全局配置把请求重新变成同步调用。 function asyncAjaxRequest(options, onDone) { var request = null; var method; var requestUrl; var requestBody = null; var dataText = ''; var pairs = []; var key; var timeoutMs = Math.max(1, Number(options && options.timeoutMs) || 20000); var completed = false; var timer = null; var requestAccountId = ''; var writeTracked = false; function finish(ok, data, error, xhr) { if (completed) return false; completed = true; if (timer) clearTimeout(timer); if (writeTracked) { writeTracked = false; finishTrackedWrite(requestAccountId); } onDone({ ok: ok, data: data, error: error || '', xhr: xhr || null }); return true; } options = options || {}; if (options.isWrite === true) { requestAccountId = normalizeAccountId(options.accountId) || getCurrentAccountId(); if (isPauseRequested(requestAccountId)) { onDone({ ok: false, data: null, error: 'paused', xhr: null }); return null; } if (!requestAccountId || !ownsExecutionLock(requestAccountId)) { onDone({ ok: false, data: null, error: 'execution lock lost', xhr: null }); return null; } } method = String(options.type || options.method || 'GET').toUpperCase(); requestUrl = String(options.url || ''); if (typeof options.data === 'string') { dataText = options.data; } else if (options.data && typeof options.data === 'object') { for (key in options.data) { if (Object.prototype.hasOwnProperty.call(options.data, key) && typeof options.data[key] !== 'undefined') { pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(options.data[key] === null ? '' : String(options.data[key]))); } } dataText = pairs.join('&').replace(/%20/g, '+'); } if (method === 'GET') { if (dataText) requestUrl += (requestUrl.indexOf('?') >= 0 ? '&' : '?') + dataText; requestUrl += (requestUrl.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now(); } else { requestBody = dataText; } try { request = new XMLHttpRequest(); request.onreadystatechange = function () { var responseData; if (request.readyState !== 4 || completed) return; if (request.status >= 200 && request.status < 300) { responseData = request.responseText; if (String(options.dataType || '').toLowerCase() === 'json') { try { responseData = JSON.parse(responseData); } catch (e) { finish(false, null, 'invalid json', request); return; } } finish(true, responseData, '', request); } else { finish(false, null, request.status ? 'HTTP ' + request.status : 'request failed', request); } }; request.onerror = function () { finish(false, null, request.status ? 'HTTP ' + request.status : 'network error', request); }; request.open(method, requestUrl, true); request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); if (method !== 'GET') { request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); } if (options.isWrite === true) { beginTrackedWrite(requestAccountId); writeTracked = true; } timer = setTimeout(function () { if (completed) return; finish(false, null, 'timeout', null); try { request.abort(); } catch (e) { } }, timeoutMs); request.send(requestBody); } catch (e) { finish(false, null, 'exception: ' + e.message, null); } return request; } function getManualStartAction(pathname) { return /^\/PersonWap\/Index/i.test(String(pathname || '')) ? 'sync-now' : 'return-index'; } // 获取首页地址(动态,不写死 Index0018) function getIndexUrl() { // 优先从 localStorage 读取之前保存的地址 try { var saved = localStorage.getItem('bmzhkt_index_url'); if (saved) return saved; } catch (e) { } // 从当前 URL 推断 var path = window.location.pathname || ''; var match = path.match(/\/PersonWap\/Index\d+/); if (match) return match[0]; // 默认回退:返回不带编号的首页路径,让服务器自动重定向到正确首页 return '/PersonWap/'; } // 保存当前首页地址 function saveIndexUrl() { var path = window.location.pathname || ''; if (path.indexOf('/PersonWap/Index') >= 0) { try { localStorage.setItem('bmzhkt_index_url', path); } catch (e) { } } } // 尝试解析 JSON function tryParseJSON(str) { try { return JSON.parse(str); } catch (e) { return null; } } function getDailyScoreLimits() { return { signInScore: DAILY_SCORE_LIMITS.signInScore, articleScore: DAILY_SCORE_LIMITS.articleScore, practiceScore: DAILY_SCORE_LIMITS.practiceScore, mobileExamScore: DAILY_SCORE_LIMITS.mobileExamScore, simulateScore: DAILY_SCORE_LIMITS.simulateScore, videoScore: DAILY_SCORE_LIMITS.videoScore }; } function normalizeDailyScoreLimits(value) { var defaults = getDailyScoreLimits(); var source = value && typeof value === 'object' ? value : {}; for (var key in defaults) { if (!Object.prototype.hasOwnProperty.call(defaults, key)) continue; var limit = Number(source[key]); if (isFinite(limit) && limit > 0) defaults[key] = limit; } return defaults; } function getDailyScoreCriterionFields() { return { CheckIn: 'signInScore', StudyKnowledge: 'articleScore', MobileExercises: 'practiceScore', MobileExam: 'mobileExamScore', MockExam: 'simulateScore', Course: 'videoScore', Studytime: 'videoScore' }; } // 只输出固定的业务项名称,不输出原始响应或账号信息。 function describeDailyScoreSnapshotFailure(response) { var value = response; if (typeof value === 'string') { var text = value.trim(); if (!text) return 'empty response'; if (/^= 0) state[field] = Math.min(score, limit); } return state; } // 只把明确的业务成功响应当作成功,避免 HTTP 200 登录页被误判。 function parseApiSuccess(response) { if (response === true || response === 1) return true; if (response === false || response === null || typeof response === 'undefined') return false; if (typeof response === 'object') { if (Object.prototype.hasOwnProperty.call(response, 'success')) { return response.success === true || response.success === 1 || response.success === 'true'; } if (Object.prototype.hasOwnProperty.call(response, 'Success')) { return response.Success === true || response.Success === 1 || response.Success === 'true'; } if (Object.prototype.hasOwnProperty.call(response, 'code')) { return response.code === 0 || response.code === 200 || response.code === '0' || response.code === '200'; } if (Object.prototype.hasOwnProperty.call(response, 'data')) { return response.data !== null && response.data !== false && response.data !== ''; } return false; } var text = String(response).trim(); if (!text || /^ERROR:/i.test(text) || /^= 1 && index <= 26 && indexes.indexOf(index) < 0) indexes.push(index); } return indexes.sort(function (a, b) { return a - b; }); } // 单选和判断题直接点正确项;多选题只切换选错或漏选的项。 function getAnswerToggleIndexes(actual, expected) { if (answersMatch(actual, expected)) return []; var expectedText = String(expected || '').trim(); var target = answerToIndexes(expectedText); if (target.length === 0) return []; if (expectedText === '对' || expectedText === '错' || expectedText.length === 1) return target; var current = answerToIndexes(actual); var toggles = []; var i; for (i = 0; i < target.length; i++) { if (current.indexOf(target[i]) < 0) toggles.push(target[i]); } for (i = 0; i < current.length; i++) { if (target.indexOf(current[i]) < 0) toggles.push(current[i]); } return toggles.sort(function (a, b) { return a - b; }); } // 整卷保存接口要求每题前 15 个字段,并以竖线连接整张试卷。 function buildExamAnswerPayload(questions) { if (!Array.isArray(questions) || questions.length === 0) return ''; var rows = []; for (var i = 0; i < questions.length; i++) { if (typeof questions[i] !== 'string') return ''; var fields = questions[i].split(','); if (fields.length < 15) return ''; rows.push(fields.slice(0, 15).join(',')); } return rows.join('|'); } // 把标准答案转换成网站提交编码后写入用户答案字段;判断题必须使用 Y/N。 function fixAllShiTiAnswers(allShiTi) { if (!Array.isArray(allShiTi)) return allShiTi; for (var i = 0; i < allShiTi.length; i++) { var st = allShiTi[i]; if (!st || typeof st !== 'string') continue; var strs = st.split(','); var submissionAnswer = strs.length > 13 ? toSubmissionAnswer(strs[13]) : ''; if (submissionAnswer && strs[4] !== submissionAnswer) { strs[4] = submissionAnswer; allShiTi[i] = strs.join(','); } } return allShiTi; } function calculatePracticePoints(answeredCount) { return Math.max(0, Math.min(15, Math.floor((Number(answeredCount) || 0) / 5))); } function parseExamScore(response) { if (!response) return null; var text = String(response).trim(); if (/^ERROR:/i.test(text) || /^= 0 && score <= 100 ? score : null; } function parseExamPassStatus(response) { if (!response) return null; var text = String(response).trim(); if (/^ERROR:/i.test(text) || /^= 100 ? 6 : 4; if (examType === 'simulate') return n >= 100 ? 6 : 4; return 0; } // 考试只要求及格并获得正积分,不要求满分。 function evaluateExamScore(examType, score) { if (score === null || typeof score === 'undefined' || score === '') { return { completed: false, score: null, points: 0 }; } var parsedScore = Number(score); if (!isFinite(parsedScore) || parsedScore < 0 || parsedScore > 100) { return { completed: false, score: null, points: 0 }; } var points = calculateExamPoints(examType, parsedScore); return { completed: points > 0, score: parsedScore, points: points }; } function evaluateExamSubmission(examType, response) { var score = parseExamScore(response); var passed = parseExamPassStatus(response); if (score === null || passed === null) { return { valid: false, completed: false, score: score, points: 0 }; } var outcome = evaluateExamScore(examType, score); return { valid: true, completed: passed && outcome.completed, score: score, points: passed ? outcome.points : 0 }; } function parseScheduledExamDate(value) { var match = String(value || '').match(/^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})$/); if (!match) return 0; var date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), 0, 0); return date.getFullYear() === Number(match[1]) && date.getMonth() === Number(match[2]) - 1 && date.getDate() === Number(match[3]) ? date.getTime() : 0; } function getScheduledExamTimeRange(timeText) { var matches = String(timeText || '').match(/\d{4}-\d{1,2}-\d{1,2}\s+\d{1,2}:\d{2}/g) || []; if (matches.length < 2) return { startAt: 0, endAt: 0 }; return { startAt: parseScheduledExamDate(matches[0]), endAt: parseScheduledExamDate(matches[1]) }; } function getScheduledExamMonth(title) { var match = String(title || '').match(/(\d{4})年\s*(\d{1,2})月(?:份)?考试/); return match ? { year: Number(match[1]), month: Number(match[2]) } : null; } function isScheduledExamCompleted(row) { var value = row && row.whetherExam; if (value === true || value === 1) return true; return /^(?:true|1|yes)$/i.test(String(value || '').trim()); } function selectScheduledExamCandidate(rows, now) { var source = Array.isArray(rows) ? rows : []; var currentTime = now instanceof Date ? now.getTime() : Number(now) || Date.now(); var currentDate = new Date(currentTime); var available = null; var completed = null; var upcoming = null; for (var i = 0; i < source.length; i++) { var row = source[i] || {}; var range = getScheduledExamTimeRange(row.timeText || row.text || ''); var candidate = { index: typeof row.index === 'number' ? row.index : i, title: String(row.title || ''), startAt: range.startAt, endAt: range.endAt }; var isCompleted = isScheduledExamCompleted(row); if (!isCompleted && range.startAt && range.endAt && range.startAt <= currentTime && currentTime <= range.endAt) { if (!available || candidate.endAt < available.endAt) available = candidate; continue; } var examMonth = getScheduledExamMonth(candidate.title); if (isCompleted && examMonth && examMonth.year === currentDate.getFullYear() && examMonth.month === currentDate.getMonth() + 1) { completed = candidate; } if (!isCompleted && range.startAt > currentTime && (!upcoming || range.startAt < upcoming.startAt)) { upcoming = candidate; } } if (available) { available.status = 'available'; return available; } if (completed) { completed.status = 'completed'; return completed; } if (upcoming) { upcoming.status = 'upcoming'; return upcoming; } return { status: 'none', index: -1, title: '', startAt: 0, endAt: 0 }; } function evaluateScheduledExamSubmission(response) { var score = parseExamScore(response); if (score === null) { var scoreMatch = String(response === null || typeof response === 'undefined' ? '' : response) .match(/(?:成绩|得分)?\s*(\d+(?:\.\d+)?)\s*分/); if (scoreMatch) { var parsedScore = Number(scoreMatch[1]); if (isFinite(parsedScore) && parsedScore >= 0 && parsedScore <= 100) score = parsedScore; } } var passed = parseExamPassStatus(response); var valid = score !== null && passed !== null; return { valid: valid, completed: valid && passed === true && score === 100, score: valid ? score : null }; } function canSubmitScheduledExamAsFullScore(answers) { if (!Array.isArray(answers) || answers.length === 0) return false; for (var i = 0; i < answers.length; i++) { var fields = String(answers[i] || '').split(','); if (fields.length <= 13 || !fields[4] || !fields[13] || !answersMatch(fields[4], fields[13])) { return false; } } return true; } function getExamSubmissionPlan(isOnlineTestOne) { return isOnlineTestOne ? { saveUrl: '/Home/DoWriteAnswerAllOne', endUrl: '/Home/EndTimeOne' } : { saveUrl: '/Home/DoWriteAnswerAll', endUrl: '/Home/EndTimeMobil' }; } function hasExplicitFailure(response) { var value = response; if (typeof response === 'string') { var text = response.trim(); var parsed = tryParseJSON(text); if (parsed !== null) { value = parsed; } else { return /(?:失败|错误|异常|拒绝|\bdenied\b|["']?(?:error|message|msg)["']?\s*[:=]\s*["']?[^"'}]*(?:fail(?:ed|ure)?|denied|error))/i.test(text); } } if (!value || typeof value !== 'object') return false; var fields = ['error', 'Error', 'message', 'Message', 'msg', 'Msg']; for (var i = 0; i < fields.length; i++) { if (!Object.prototype.hasOwnProperty.call(value, fields[i])) continue; var fieldText = String(value[fields[i]] === null || typeof value[fields[i]] === 'undefined' ? '' : value[fields[i]]).trim(); if (/(?:失败|错误|异常|拒绝|\bfail(?:ed|ure)?\b|\berror\b|\bdenied\b)/i.test(fieldText)) return true; } return false; } // 网站原生 SaveDA 不读取保存响应正文;HTTP 成功后的最终业务结果由 ETime 返回的成绩确认。 function canContinueAfterExamSave(requestOk, response) { if (!requestOk) return false; var text = String(response === null || typeof response === 'undefined' ? '' : response).trim(); if (text === '') { return true; } if (hasExplicitFailure(response)) return false; return parseApiSuccess(response); } function getSimulateCompletedCount(state) { var source = state && typeof state === 'object' ? state : {}; if (typeof source.simulateCompletedCount !== 'undefined') { var explicitCount = Number(source.simulateCompletedCount); if (isFinite(explicitCount) && explicitCount >= 0) { return Math.min(2, Math.floor(explicitCount)); } } var score = Math.max(0, Number(source.simulateScore) || 0); if (score >= 6) return 2; if (score >= 3) return 1; return 0; } function getDailyTaskPlan(state) { var source = state && typeof state === 'object' ? state : {}; var limits = normalizeDailyScoreLimits(source.dailyLimits); var definitions = [ { id: 'signin', field: 'signInScore', label: '登录签到' }, { id: 'article', field: 'articleScore', label: '知识学习' }, { id: 'practice', field: 'practiceScore', label: '手机练习' }, { id: 'mobile', field: 'mobileExamScore', label: '手机考试' }, { id: 'simulate', field: 'simulateScore', label: '模拟考试' } ]; var items = []; var pendingTaskIds = []; var pendingLabels = []; var total = 0; var targetTotal = 0; var completedCount = 0; for (var i = 0; i < definitions.length; i++) { var definition = definitions[i]; var limit = Number(limits[definition.field]) || 0; var current = Math.max(0, Math.min(limit, Number(source[definition.field]) || 0)); var remaining = Math.max(0, limit - current); var completed = remaining === 0; total += current; targetTotal += limit; if (completed) completedCount++; else { pendingTaskIds.push(definition.id); pendingLabels.push(definition.label); } items.push({ id: definition.id, label: definition.label, current: current, limit: limit, remaining: remaining, completed: completed }); } return { total: total, targetTotal: targetTotal, completedCount: completedCount, pendingTaskIds: pendingTaskIds, pendingLabels: pendingLabels, items: items }; } function getWorkflowSummary(state) { var source = state && typeof state === 'object' ? state : {}; var limits = normalizeDailyScoreLimits(source.dailyLimits); var signInScore = Math.max(0, Math.min(limits.signInScore, Number(source.signInScore) || 0)); var articleScore = Math.max(0, Math.min(limits.articleScore, Number(source.articleScore) || 0)); var practiceScore = Math.max(0, Math.min(limits.practiceScore, Number(source.practiceScore) || 0)); var mobileExamScore = Math.max(0, Math.min(limits.mobileExamScore, Number(source.mobileExamScore) || 0)); var simulateScore = Math.max(0, Math.min(limits.simulateScore, Number(source.simulateScore) || 0)); var nextStep = 0; if (signInScore < limits.signInScore) nextStep = 0; else if (articleScore < limits.articleScore) nextStep = 1; else if (practiceScore < limits.practiceScore) nextStep = 2; else if (mobileExamScore < limits.mobileExamScore) nextStep = 3; else if (simulateScore < limits.simulateScore) nextStep = 4; else nextStep = 6; var normalizedSkipped = normalizeSkippedTasks(source.skippedTasks); var skippedCount = normalizedSkipped.length; var total = signInScore + articleScore + practiceScore + mobileExamScore + simulateScore; var targetTotal = limits.signInScore + limits.articleScore + limits.practiceScore + limits.mobileExamScore + limits.simulateScore; return { completed: nextStep === 6, allSucceeded: nextStep === 6, total: total, nextStep: nextStep, reachedMinimum: total >= targetTotal, skippedCount: skippedCount }; } function parseFirstIndexTotal(response) { var text = String(response === null || typeof response === 'undefined' ? '' : response).trim(); if (!text || /^= 0 ? total : null; } function getConfirmedSignInScore(total) { var parsed = Number(total); return isFinite(parsed) && parsed >= 1 ? 1 : 0; } function getServerDailyTotal(pid, onDone) { if (!pid) { onDone(null, 'no pid'); return; } asyncAjaxRequest({ url: '/PersonWap/FirstIndexOne', type: 'POST', dataType: 'text', data: { pid: pid, wx: typeof M_WX !== 'undefined' ? M_WX : '0' }, timeoutMs: 20000 }, function (result) { onDone(result.ok ? parseFirstIndexTotal(result.data) : null, result.error || 'invalid response'); }); } function getDailyScoreSnapshot(pid, onDone) { if (!pid) { onDone(null, 'no pid'); return; } asyncAjaxRequest({ url: '/AccumulateManger/S_Accumulate/GetPersonTodayAccumulateOne', type: 'GET', dataType: 'json', data: { pid: Esdt(pid) }, timeoutMs: 20000 }, function (result) { if (!result.ok) { onDone(null, result.error || 'request failed'); return; } var snapshot = parseDailyScoreSnapshot(result.data); onDone(snapshot, snapshot ? '' : describeDailyScoreSnapshotFailure(result.data)); }); } // 1.5.x 曾把及格但非满分记录为错误;升级时移除这类旧误报。 function clearLegacyPassingScoreErrors(errors) { var source = Array.isArray(errors) ? errors : []; return source.filter(function (message) { var match = String(message || '').match(/: server score\s+(\d+(?:\.\d+)?)/i); return !match || Number(match[1]) < 60; }); } function remainingArticleDelay(lastUploadAt, now, interval) { var wait = Number(interval) || 60000; var last = Number(lastUploadAt) || 0; if (!last) return wait; return Math.max(0, wait - ((Number(now) || Date.now()) - last)); } // 检测服务器返回的"当天积分已达上限"提示,用于跳过已完成的任务 function isScoreLimitReached(respText) { var t = String(respText || ''); if (!t) return false; return t.indexOf('最多积分') >= 0 || t.indexOf('已达上限') >= 0 || t.indexOf('已满') >= 0; } // 成功重试后只移除当前步骤以前留下的错误,保留其他步骤的错误。 function clearErrorsByPrefixes(errors, prefixes) { var source = Array.isArray(errors) ? errors : []; var targets = Array.isArray(prefixes) ? prefixes : []; var result = []; for (var i = 0; i < source.length; i++) { var message = String(source[i] || ''); var matched = false; for (var j = 0; j < targets.length; j++) { if (message.indexOf(String(targets[j] || '')) === 0) { matched = true; break; } } if (!matched) result.push(message); } return result; } // 练习入口与考试入口存在同一种旧接口矛盾响应。矛盾响应只允许进入 // 练习页核实,最终仍由有效题目和明确提交成功决定是否记分及推进。 function getPracticeSessionAction(response) { if (parseApiSuccess(response)) return 'ready'; var value = response; if (typeof response === 'string') { try { value = JSON.parse(response); } catch (e) { value = null; } } if (value && typeof value === 'object' && value.success === false && String(value.message || '').trim() === '操作成功') { return 'probe'; } return 'retry'; } function getPracticeEntryAction(hasButton, hasMuiTrigger) { if (!hasButton) return 'fail'; return hasMuiTrigger ? 'mui-tap' : 'dom-tap'; } function getExamNativeEntryPlan(examType) { if (examType !== 'mobile' && examType !== 'simulate') return null; return { buttonId: examType === 'mobile' ? 'mainMobileExam' : 'mainSimulate', listSelector: '#canRunExamList .mui-table-view-cell[PaperId]', unfinishedListSelector: '#testList .mui-table-view-cell[ExamDetailId]', unfinishedButtonSelector: 'button[onclick*="LookMX"]', unfinishedType: examType === 'mobile' ? 2 : 3 }; } function getExamPaperIndex(examType, state, paperCount) { var count = Math.max(0, Math.floor(Number(paperCount) || 0)); if (!count || (examType !== 'mobile' && examType !== 'simulate')) return 0; var source = state && typeof state === 'object' ? state : {}; var value = examType === 'mobile' ? source.mobileExamAttemptIndex : source.simulateExamAttemptIndex; return Math.max(0, Math.floor(Number(value) || 0)) % count; } function findUnfinishedExamId(rows, paperId) { var expectedPaperId = String(paperId || '').trim(); if (!expectedPaperId || !rows || typeof rows.length !== 'number') return ''; for (var i = 0; i < rows.length; i++) { var row = rows[i] || {}; if (String(row.PaperId || '').trim() === expectedPaperId && row.ExamDetailId) { return String(row.ExamDetailId).trim(); } } return ''; } function findMatchingExamItem(items, examDetailId) { var expectedId = String(examDetailId || '').trim(); if (!expectedId || !items) return null; for (var i = 0; i < items.length; i++) { var currentId = items[i] && typeof items[i].getAttribute === 'function' ? String(items[i].getAttribute('ExamDetailId') || '').trim() : ''; if (currentId === expectedId) return items[i]; } return null; } // 考试页兜底读取 ksmxid:页面全局优先,localStorage 兜底 function resolveExamKsmxid(pageKsmxid, savedKsmxid) { var k = String(pageKsmxid || '').trim(); if (k) return k; return String(savedKsmxid || '').trim(); } // 检测考试页服务器报错(如"考试信息未找到") function isExamPageError(vData) { if (!vData || typeof vData !== 'object') return false; var msg = vData.ErrorMsg || vData.errorMsg || ''; return !!msg; } function triggerNativeTap(element) { if (!element) return false; if (typeof mui !== 'undefined' && mui && typeof mui.trigger === 'function') { mui.trigger(element, 'tap'); return true; } var tapEvent = document.createEvent('Event'); tapEvent.initEvent('tap', true, true); element.dispatchEvent(tapEvent); return true; } function readScheduledExamRows() { var items = document.querySelectorAll('li[ExamDetailId], li[examdetailid]'); var rows = []; for (var i = 0; i < items.length; i++) { var item = items[i]; var text = String(item.innerText || item.textContent || '').replace(/\s+/g, ' ').trim(); var titleMatch = text.match(/.*?\d{4}年\s*\d{1,2}月(?:份)?考试(?:([^)]*))?/); rows.push({ index: i, title: titleMatch ? titleMatch[0].trim() : text, timeText: text, whetherExam: item.getAttribute('whetherexam'), examState: item.getAttribute('examstate') }); } return rows; } function applyScheduledExamCandidate(state, candidate) { if (isPauseRequested(state.accountId)) { log('【月考】当前已暂停,检测结果暂不推进流程。'); return; } state.status = candidate.status; state.title = candidate.title || ''; state.startAt = candidate.startAt || 0; state.endAt = candidate.endAt || 0; state.selectedIndex = typeof candidate.index === 'number' ? candidate.index : -1; state.message = ''; state.active = candidate.status === 'available'; state.phase = candidate.status === 'available' ? 'running' : 'idle'; saveMonthlyExamState(state); renderMonthlyExamPanel(state); if (candidate.status === 'available') { log('【月考】已检测到当前开放场次,正在自动进入试卷答题。'); runMonthlyExamFlow(state); return; } releaseExecutionLock(state.accountId); if (candidate.status === 'completed') { log('【月考】本月安排考试已完成,不会重复提交。'); } else if (candidate.status === 'upcoming') { log('【月考】已找到后续场次,但当前尚未开放。'); } else { log('【月考】当前没有可参加的安排考试。'); } } function failMonthlyExamAction(state, message) { if (isPauseRequested(state && state.accountId)) { log('【月考】当前已暂停,不自动结束、重试或跳页。'); return; } finishMonthlyExamAction(state, 'error', message); log('【月考】' + message); } function handleMonthlyExamPanelAction() { var accountId = getCurrentAccountId(); if (!accountId) { log('【月考】请回到已登录的智慧课堂首页后再操作。'); return; } if (isPauseRequested(accountId)) { log('【月考】当前总控任务已暂停,请先点击“继续”后再检测月考。'); return; } if (isRunActive(accountId)) { log('【月考】每日任务正在运行,请先暂停或等待每日任务结束。'); return; } var state = getMonthlyExamState(accountId); if (state.active) { log('【月考】操作正在进行,请勿重复点击。'); return; } if (!startLockHeartbeat(accountId)) { log('【月考】另一个标签页正在执行当前账号,请关闭或停止另一个标签页后再试。'); return; } state.active = true; state.status = 'unknown'; state.phase = 'detecting'; state.message = ''; log('【月考】正在打开“安排的考试”列表检测...'); saveMonthlyExamState(state); renderMonthlyExamPanel(state); start(); } function getScheduledExamStartElement(item) { if (!item) return null; var clickable = item.querySelectorAll('button, a'); for (var i = 0; i < clickable.length; i++) { var text = String(clickable[i].innerText || clickable[i].textContent || ''); if (/(?:未参加|参加考试|开始考试|继续考试)/.test(text)) return clickable[i]; } return item; } function runMonthlyExamFlow(state) { if (isPauseRequested(state.accountId)) { log('【月考】当前已暂停,等待点击“继续”。'); return; } var path = window.location.pathname || ''; if (path.indexOf('/P_ExamDetail/OnlineTest') >= 0 && path.indexOf('OnlineTestOne') < 0) { if (state.phase !== 'running') { failMonthlyExamAction(state, '月考状态异常,未执行交卷。'); return; } doExamTask(null, state); return; } if (path.indexOf('/PersonWap/Index') < 0) { failMonthlyExamAction(state, '未在首页或月考试卷页,未执行答题。'); return; } var rows = readScheduledExamRows(); if (state.phase === 'detecting') { if (rows.length) { applyScheduledExamCandidate(state, selectScheduledExamCandidate(rows, new Date())); return; } var entryButton = document.getElementById('mainPlanSimulate'); if (!entryButton) { failMonthlyExamAction(state, '未找到网页“安排的考试”入口。'); return; } try { triggerNativeTap(entryButton); } catch (e) { failMonthlyExamAction(state, '打开考试列表失败。'); return; } waitFor(function () { return readScheduledExamRows().length > 0; }, 20000, function (ready) { if (!ready) { failMonthlyExamAction(state, '考试列表加载超时,请重新检测。'); return; } applyScheduledExamCandidate(state, selectScheduledExamCandidate(readScheduledExamRows(), new Date())); }); return; } if (state.phase === 'running') { var candidate = selectScheduledExamCandidate(rows, new Date()); if (candidate.status !== 'available') { applyScheduledExamCandidate(state, candidate); return; } var items = document.querySelectorAll('li[ExamDetailId], li[examdetailid]'); var target = items[candidate.index]; try { var startElement = getScheduledExamStartElement(target); if (!startElement || !triggerNativeTap(startElement)) { failMonthlyExamAction(state, '未找到当前月考的进入按钮。'); return; } } catch (e) { failMonthlyExamAction(state, '进入当前月考失败。'); return; } setTimeout(function () { var latest = getMonthlyExamState(state.accountId); var latestPath = window.location.pathname || ''; if (!isPauseRequested(state.accountId) && latest.active && latest.phase === 'running' && latestPath.indexOf('/P_ExamDetail/OnlineTest') < 0) { failMonthlyExamAction(latest, '进入月考试卷超时,请重新检测。'); } }, 20000); return; } failMonthlyExamAction(state, '月考流程状态无效,请重新检测。'); } // 与页面原生 Esdt 保持一致,供油猴隔离环境使用。 function Esdt(code) { var c = ""; var l = []; for (var i = 0; i <= code.length - 1; i++) { var zf; if (code[i] == null) zf = code.charAt(i); else zf = code[i]; var temp = zf.charCodeAt(); l.push(temp.toString().length); c += temp; } return escape(c + "^" + l.join(",")); } // ========== 登录签到确认(1分) ========== function confirmSignInTask(state, onDone) { getDailyScoreSnapshot(typeof M_PersonId !== 'undefined' ? M_PersonId : '', function (snapshot, error) { if (!snapshot || snapshot.scores.signInScore < snapshot.limits.signInScore) { state.errors.push('signin: score not confirmed'); log('【登录签到】服务器尚未确认签到积分,暂不记分' + (error ? ':' + error : '')); onDone(false); return; } applyDailyScoreSnapshot(state, snapshot); state.serverDailyTotal = getWorkflowSummary(state).total; state.errors = clearErrorsByPrefixes(state.errors, ['signin:']); saveState(state); dbg('[登录签到] 服务器积分明细确认签到已完成'); log('【登录签到】服务器已确认 1/1 分'); onDone(true); }); } // ========== 读文章任务(每分钟1分,每天15分) ========== function doArticleTask(state, onDone) { log('【读文章】开始执行...'); var articleLimit = normalizeDailyScoreLimits(state.dailyLimits).articleScore; dbg('[读文章] 开始, 已有积分=' + state.articleScore + '/' + articleLimit); var pid = M_PersonId; if (!pid) { log('错误:M_PersonId 为空,请先登录'); dbg('[读文章] 失败: M_PersonId为空'); state.errors.push('article: no pid'); onDone(false); return; } var encPid = Esdt(pid); log('【读文章】获取文章分类...'); asyncAjaxRequest({ url: '/LearnManger/L_KnowledgeType/GetAllTypeOne', type: 'GET', dataType: 'text', data: { pid: encPid }, timeoutMs: 20000 }, function (typeResult) { var typeResp = typeResult.ok ? String(typeResult.data || '') : ''; dbg('[读文章] 分类响应: ' + (typeResult.ok ? dbgResp(typeResp) : typeResult.error)); if (!typeResult.ok || !typeResp) { log('【读文章】获取分类失败:' + (typeResult.error || '空响应')); state.errors.push('article: getTypes failed'); onDone(false); return; } var types = tryParseJSON(typeResp); if (types && types.data) types = types.data; if (!types || !Array.isArray(types) || types.length === 0) { log('【读文章】没有找到文章分类'); state.errors.push('article: no types'); onDone(false); return; } var typeId = types[0].KnowledgeTypeId || types[0].Id || types[0].id || types[0].ID || ''; if (!typeId) { log('【读文章】分类 ID 为空'); state.errors.push('article: no typeId'); onDone(false); return; } log('【读文章】分类ID: ' + typeId); log('【读文章】获取文章列表...'); asyncAjaxRequest({ url: '/LearnManger/L_KnowledgeSubject/GetStudyOne', type: 'GET', dataType: 'text', data: { typeid: typeId, pid: pid, page: 1, rows: 20 }, timeoutMs: 20000 }, function (articleResult) { var artResp = articleResult.ok ? String(articleResult.data || '') : ''; dbg('[读文章] 文章列表响应: ' + (articleResult.ok ? dbgResp(artResp) : articleResult.error)); if (!articleResult.ok || !artResp) { log('【读文章】获取文章列表失败:' + (articleResult.error || '空响应')); state.errors.push('article: getArticles failed'); onDone(false); return; } var articles = tryParseJSON(artResp); if (articles && articles.data) articles = articles.data; if (!articles || !Array.isArray(articles) || articles.length === 0) { log('【读文章】没有找到文章'); state.errors.push('article: no articles'); onDone(false); return; } var successCount = Math.max(0, Math.min(articleLimit, Number(state.articleScore) || 0)); function uploadNext() { if (successCount >= articleLimit) { log('【读文章】完成,已确认 ' + articleLimit + '/' + articleLimit + ' 次有效响应'); dbg('[读文章] 完成, 积分=' + articleLimit + '/' + articleLimit); onDone(true); return; } var delay = remainingArticleDelay(state.articleLastUploadAt, Date.now(), 60000); log('【读文章】第 ' + (successCount + 1) + '/' + articleLimit + ' 次将在 ' + Math.ceil(delay / 1000) + ' 秒后上传'); setTimeout(function () { if (isPauseRequested(state.accountId) || !isRunActive(state.accountId)) { log('已暂停,读文章停在第 ' + successCount + '/' + articleLimit + ' 次前。点"继续"恢复。'); return; } var article = articles[successCount % articles.length]; var articleId = article.KnowledgeSubjectId || article.Id || article.id || article.ID || ''; if (!articleId) { state.errors.push('article: no articleId'); saveState(state); log('【读文章】文章 ID 为空,已停止,刷新后可重试'); onDone(false); return; } state.articleLastUploadAt = Date.now(); saveState(state); var pending = makePendingWrite(state, 'article-upload', 1); asyncAjaxRequest({ url: '/ArchiveManger/D_PersonAccumulate/StudyKnowledgeOne', type: 'GET', dataType: 'text', data: { pid: encPid, zsid: Esdt(articleId) }, timeoutMs: 20000, isWrite: true, accountId: state.accountId }, function (uploadResult) { var resp = uploadResult.ok ? String(uploadResult.data || '') : ''; dbg('[读文章] 第' + (successCount + 1) + '次上传响应: ' + (uploadResult.ok ? dbgResp(resp) : uploadResult.error)); if (!uploadResult.ok || !parseApiSuccess(resp)) { if (getRequestFailureAction(uploadResult.error, true) === 'reconcile') { handleAmbiguousWrite(state, pending, '读文章'); return; } if (uploadResult.ok && isScoreLimitReached(resp)) { log('【读文章】当天已达上限,跳过读文章'); dbg('[读文章] 当天已达上限, 跳过'); state.articleLimitReached = true; saveState(state); onDone(true); return; } state.errors.push('article: upload failed'); saveState(state); log('【读文章】上传未得到明确成功响应,已停止:' + (uploadResult.error || resp.substring(0, 80))); onDone(false); return; } successCount++; state.articleScore = successCount; saveState(state); log('【读文章】第 ' + successCount + '/' + articleLimit + ' 次上传已确认'); getServerDailyTotal(pid, function (latestTotal) { if (isFinite(Number(latestTotal))) { state.serverDailyTotal = Number(latestTotal); saveState(state); } uploadNext(); }); }); }, delay); } uploadNext(); }); }); return true; } // ========== 练习题任务(每5题1分,每天15分) ========== // 进入练习页(在首页调用)。复用网站原生按钮,保留其内部选题和会话参数。 function enterPractice(state) { log('【练习题】准备进入练习...'); saveVars(); var button = document.getElementById('mainSequenceA'); var hasMuiTrigger = typeof mui !== 'undefined' && mui && typeof mui.trigger === 'function'; var entryAction = getPracticeEntryAction(!!button, !!hasMuiTrigger); if (entryAction === 'fail') { log('【练习题】未找到网页的顺序练习入口,已停止并准备重试'); state.errors.push('practice: entry button missing'); handleStepFailure(state); return false; } try { dbg('[练习题] 进入方式: 网页原生按钮'); log('【练习题】正在通过网页原生入口建立练习会话...'); if (entryAction === 'mui-tap') mui.trigger(button, 'tap'); else { var tapEvent = document.createEvent('Event'); tapEvent.initEvent('tap', true, true); button.dispatchEvent(tapEvent); } setTimeout(function () { var currentState = getState(state.accountId); var stillOnIndex = String(window.location.pathname || '').indexOf('/PersonWap/Index') >= 0; if (!stillOnIndex || currentState.step !== 2 || !isRunActive(state.accountId) || isPauseRequested(state.accountId)) return; log('【练习题】网页原生入口 20 秒内没有打开练习页,已停止并准备重试'); currentState.errors.push('practice: entry navigation timeout'); handleStepFailure(currentState); }, 20000); } catch (e) { log('【练习题】网页原生入口触发失败:' + e.message); state.errors.push('practice: entry trigger failed'); handleStepFailure(state); return false; } return true; } // 在练习页答题并提交 function doPracticeTask(state) { log('【练习题】练习页加载中,等待题目数据...'); // 等待 allShiTi 加载 waitFor(function () { return typeof allShiTi !== 'undefined' && allShiTi && allShiTi.length > 0; }, 30000, function (ok) { if (isPauseRequested(state.accountId)) { log('【练习题】已暂停,题目不会提交。'); return; } if (!ok) { log('【练习题】题目数据加载超时'); state.errors.push('practice: allShiTi timeout'); handleStepFailure(state); return; } log('【练习题】题目数据已加载,共 ' + allShiTi.length + ' 题'); var practiceLimit = normalizeDailyScoreLimits(state.dailyLimits).practiceScore; var remainingQuestions = Math.max(0, practiceLimit - state.practiceScore) * 5; dbg('[练习题] 题目数=' + allShiTi.length + ', 本次目标=' + Math.min(remainingQuestions, allShiTi.length)); // 保存练习页的变量 saveVars(); loadVars(state.accountId); // 一页题目不足以补满时,提交后回首页同步积分,再进入下一轮。 var target = Math.min(remainingQuestions, allShiTi.length); var answered = 0; if (typeof move2 !== 'function' || typeof SD !== 'function') { log('【练习题】页面答题函数缺失,已停止且不会提交'); state.errors.push('practice: move2 or SD missing'); handleStepFailure(state); return; } for (var i = 1; i <= target; i++) { try { // 切换到第 i 题 move2(i); // 读取标准答案 var bzda = ''; var bzdaEl = document.getElementById(i + 'bzda'); if (bzdaEl) { bzda = bzdaEl.value; } else if (allShiTi[i - 1]) { // 从 allShiTi 读取 strs[13] var strs = allShiTi[i - 1].split(','); if (strs.length > 13) bzda = strs[13]; } if (!bzda) { continue; } // 检查是否已答对 var sjda = ''; var sjdaEl = document.getElementById(i + 'sjda'); if (sjdaEl) sjda = sjdaEl.value; else if (allShiTi[i - 1]) { var strs2 = allShiTi[i - 1].split(','); if (strs2.length > 4) sjda = strs2[4]; } var alreadyMatch = answersMatch(sjda, bzda); if (alreadyMatch) { answered++; continue; } // 调用 SD 选中答案 var style; if (bzda === '对' || bzda === '错') style = '3'; else if (bzda.length > 1) style = '2'; else style = '1'; var toggleIndexes = getAnswerToggleIndexes(sjda, bzda); for (var j = 0; j < toggleIndexes.length; j++) { SD(i + '|' + toggleIndexes[j], i, 'False', style, i.toString()); } var current = sjdaEl ? sjdaEl.value : ''; if (!current && allShiTi[i - 1]) { var updated = allShiTi[i - 1].split(','); current = updated[4] || ''; } if (answersMatch(current, bzda)) answered++; } catch (e) { log('【练习题】第 ' + i + ' 题答题异常:' + e.message); } } log('【练习题】答题完成,验证正确 ' + answered + '/' + target + ' 题'); dbg('[练习题] 答题结果: 正确' + answered + '/' + target); if (answered !== target) { state.errors.push('practice: verified ' + answered + '/' + target); log('【练习题】存在未验证正确的题目,本次不会提交,将重试或停止本轮'); handleStepFailure(state); return; } setTimeout(function () { if (isPauseRequested(state.accountId)) { log('【练习题】已暂停,答案已保留在页面但不会提交。'); return; } submitPractice(state, answered); }, 0); }); } // 提交练习题答案 function submitPractice(state, answeredCount) { log('【练习题】提交答案...'); var vars = loadVars(state.accountId); var pid = M_PersonId || vars.M_PersonId || ''; var examtypeid = M_ExamTypeId || vars.M_ExamTypeId || ''; var code = M_Code || vars.M_Code || ''; if (!code) { log('【练习题】警告:M_Code 为空,提交可能失败'); } var mystyle = (typeof M_Style !== 'undefined') ? M_Style : '0'; var stid = M_StudyTimeAccumulateId || vars.M_StudyTimeAccumulateId || ''; var meid = M_MobileExercisesAccumulateId || vars.M_MobileExercisesAccumulateId || ''; // 构建 pparms var pparms = [ (typeof M_StudyTimeAccumulate !== 'undefined' ? M_StudyTimeAccumulate : (vars.M_StudyTimeAccumulate || '')), (typeof M_StudyTimeOneMaxAccumulate !== 'undefined' ? M_StudyTimeOneMaxAccumulate : (vars.M_StudyTimeOneMaxAccumulate || '')), (typeof M_StudyTimeMaxAccumulate !== 'undefined' ? M_StudyTimeMaxAccumulate : (vars.M_StudyTimeMaxAccumulate || '')), (typeof M_MobileExercisesAccumulate !== 'undefined' ? M_MobileExercisesAccumulate : (vars.M_MobileExercisesAccumulate || '')), (typeof M_MobileExercisesOneMaxAccumulate !== 'undefined' ? M_MobileExercisesOneMaxAccumulate : (vars.M_MobileExercisesOneMaxAccumulate || '')), (typeof M_MobileExercisesMaxAccumulate !== 'undefined' ? M_MobileExercisesMaxAccumulate : (vars.M_MobileExercisesMaxAccumulate || '')), '' ].join('|'); // 构建 questionids(格式:题号,题目UUID,用户答案,用时|...) // 用 allShiTi.length 确保所有题都提交,没答的题服务器自然不给分 var hasST = (typeof allShiTi !== 'undefined' && allShiTi); var count = hasST ? allShiTi.length : (answeredCount > 0 ? answeredCount : 30); var questionids = ''; for (var i = 1; i <= count; i++) { if (!hasST || !allShiTi[i - 1]) continue; var strs = allShiTi[i - 1].split(','); var qid = strs[1] || ''; // 题目 UUID var ans = strs[4] || ''; // 用户答案 questionids += i + ',' + qid + ',' + ans + ',0|'; } var pending = makePendingWrite(state, 'practice-submit', calculatePracticePoints(answeredCount)); asyncAjaxRequest({ url: '/ArchiveManger/D_PersonStuday/SetBatQuestionOne', type: 'POST', dataType: 'text', data: { questionids: questionids, pid: pid, examtypeid: examtypeid, code: code, pparms: pparms, mystyle: mystyle, StudyTimeAccumulateId: stid, MobileExercisesAccumulateId: meid }, timeoutMs: 60000, isWrite: true, accountId: state.accountId }, function (result) { var resp = result.ok ? String(result.data || '') : ''; dbg('[练习题] 提交响应: ' + (result.ok ? dbgResp(resp) : result.error)); if (!result.ok || !parseApiSuccess(resp)) { if (getRequestFailureAction(result.error, true) === 'reconcile') { handleAmbiguousWrite(state, pending, '练习题'); return; } if (result.ok && isScoreLimitReached(resp)) { log('【练习题】当天练习积分已达上限,跳过练习'); dbg('[练习题] 当天已达上限, 相关项目按完成处理'); state.errors = clearErrorsByPrefixes(state.errors, ['practice:']); state.exerciseLimitReached = true; state.step = getWorkflowSummary(state).nextStep; state.stepRetries = 0; saveState(state); setTimeout(function () { if (!isPauseRequested(state.accountId)) location.href = getIndexUrl(); }, 2000); return; } log('【练习题】提交未得到明确成功响应:' + (result.error || resp.substring(0, 100))); state.errors.push('practice: submit failed'); handleStepFailure(state); return; } state.errors = clearErrorsByPrefixes(state.errors, ['practice:']); var practiceLimit = normalizeDailyScoreLimits(state.dailyLimits).practiceScore; var earnedPoints = calculatePracticePoints(answeredCount); state.practiceScore = Math.min(practiceLimit, state.practiceScore + earnedPoints); dbg('[练习题] 提交成功, 临时积分=' + state.practiceScore + '/' + practiceLimit); log('【练习题】提交成功;返回首页后将按服务器积分明细核对是否达到 ' + practiceLimit + ' 分'); state.step = getWorkflowSummary(state).nextStep; state.stepRetries = 0; saveState(state); log('【练习题】任务结束,跳转首页...'); setTimeout(function () { if (!isPauseRequested(state.accountId)) location.href = getIndexUrl(); }, 2000); }); } // ========== 考试任务 ========== function submitExamAnswers(state, stepName, examType, ksmxid, answerPayload, context) { var source = context || '考试'; var submissionPlan = getExamSubmissionPlan(true); var estimatedPoints = 4; var savePending = makePendingWrite(state, 'exam-save', estimatedPoints); log('【' + stepName + '】保存整张试卷并准备交卷...'); asyncAjaxRequest({ url: submissionPlan.saveUrl, type: 'POST', dataType: 'text', data: { answer: answerPayload, ksmxid: ksmxid }, timeoutMs: 20000, isWrite: true, accountId: state.accountId }, function (saveResult) { dbg('[' + stepName + '][' + source + '] 保存响应: ' + (saveResult.ok ? dbgResp(saveResult.data) : saveResult.error)); if (!canContinueAfterExamSave(saveResult.ok, saveResult.data)) { if (getRequestFailureAction(saveResult.error, true) === 'reconcile') { handleAmbiguousWrite(state, savePending, stepName + '保存'); return; } log('【' + stepName + '】服务器未确认整卷保存' + (saveResult.error ? ':' + saveResult.error : '')); state.errors.push(stepName + ': batch save failed'); handleStepFailure(state); return; } if (isPauseRequested(state.accountId)) { log('【' + stepName + '】已暂停,整卷保存已返回;暂不发送交卷请求。'); return; } log('【' + stepName + '】整卷保存请求已完成,提交交卷并读取服务器分数...'); var endPending = makePendingWrite(state, 'exam-end', estimatedPoints); asyncAjaxRequest({ url: submissionPlan.endUrl, type: 'POST', dataType: 'text', data: { ksmxid: ksmxid }, timeoutMs: 20000, isWrite: true, accountId: state.accountId }, function (endResult) { var examOutcome = endResult.ok ? evaluateExamSubmission(examType, endResult.data) : { valid: false, completed: false, score: null, points: 0 }; dbg('[' + stepName + '][' + source + '] 交卷响应: ' + (endResult.ok ? dbgResp(endResult.data) : endResult.error) + ', 解析分数=' + (examOutcome.score === null ? '无效' : examOutcome.score)); if (!examOutcome.valid) { if (getRequestFailureAction(endResult.error, true) === 'reconcile') { handleAmbiguousWrite(state, endPending, stepName + '交卷'); return; } log('【' + stepName + '】交卷未返回有效分数,状态未推进' + (endResult.error ? ':' + endResult.error : '')); state.errors.push(stepName + ': invalid submit result'); handleStepFailure(state); return; } if (!examOutcome.completed) { log('【' + stepName + '】服务器成绩 ' + examOutcome.score + ' 分且判定不合格,未取得积分'); state.errors.push(stepName + ': not passed'); handleStepFailure(state); return; } state.errors = clearErrorsByPrefixes(state.errors, [stepName + ':']); var limits = normalizeDailyScoreLimits(state.dailyLimits); if (examType === 'mobile') { state.mobileExamScore = Math.min(limits.mobileExamScore, state.mobileExamScore + examOutcome.points); state.mobileExamAttemptIndex = Math.max(0, Number(state.mobileExamAttemptIndex) || 0) + 1; } else { state.simulateScore = Math.min(limits.simulateScore, state.simulateScore + examOutcome.points); state.simulateExamAttemptIndex = Math.max(0, Number(state.simulateExamAttemptIndex) || 0) + 1; } dbg('[' + stepName + '][' + source + '] 成绩=' + examOutcome.score + '分, 积分=' + examOutcome.points + '分'); log('【' + stepName + '】服务器成绩 ' + examOutcome.score + ' 分,本项积分 ' + examOutcome.points + ' 分,已完成'); finishExam(state, stepName); }); }); } function submitMonthlyExamAnswers(state, ksmxid, answerPayload) { var submissionPlan = getExamSubmissionPlan(false); log('【月考】保存整张试卷并准备交卷...'); asyncAjaxRequest({ url: submissionPlan.saveUrl, type: 'POST', dataType: 'text', data: { answer: answerPayload, ksmxid: ksmxid }, timeoutMs: 20000, isWrite: true, accountId: state.accountId }, function (saveResult) { if (isPauseRequested(state.accountId)) { log('【月考】已暂停,整卷保存已返回;暂不发送交卷请求。'); return; } if (!canContinueAfterExamSave(saveResult.ok, saveResult.data)) { failMonthlyExamAction(state, '服务器未确认月考试卷保存,请重新检测后核实。'); return; } log('【月考】整卷保存请求已完成,正在提交交卷并读取服务器分数...'); asyncAjaxRequest({ url: submissionPlan.endUrl, type: 'POST', dataType: 'text', data: { ksmxid: ksmxid }, timeoutMs: 20000, isWrite: true, accountId: state.accountId }, function (endResult) { var outcome = endResult.ok ? evaluateScheduledExamSubmission(endResult.data) : { valid: false, completed: false, score: null }; if (!outcome.valid) { failMonthlyExamAction(state, '交卷未返回有效分数,请重新检测后核实。'); return; } state.score = outcome.score; finishMonthlyExamAction(state, outcome.completed ? 'completed' : 'submitted', ''); if (outcome.completed) { log('【月考】服务器已交卷,成绩 100 分,月考完成。'); } else { log('【月考】服务器已交卷,成绩 ' + outcome.score + ' 分;未达到 100 分,不标记月考完成。'); } }); }); } // 进入考试:复用网站原生入口和列表项,由当前页面建立完整考试会话。 // examType: 'mobile' 或 'simulate' function enterExam(state, examType) { var stepName = state.step === 3 ? '手机考试' : '模拟考试'; if (!M_PersonId) { log('错误:M_PersonId 为空'); state.errors.push(stepName + ': no pid'); handleStepFailure(state); return false; } var examName = (examType === 'mobile') ? '手机考试' : '模拟考试'; var entryPlan = getExamNativeEntryPlan(examType); var entryButton = entryPlan ? document.getElementById(entryPlan.buttonId) : null; if (!entryButton) { log('【' + examName + '】未找到网页原生入口,已停止并准备重试'); state.errors.push(stepName + ': native entry missing'); handleStepFailure(state); return false; } var entryFinished = false; var navigationTimer = null; function clearNavigationTimer() { if (!navigationTimer) return; clearTimeout(navigationTimer); navigationTimer = null; } function failEntry(currentState, message, reason) { if (entryFinished) return; entryFinished = true; clearNavigationTimer(); log(message); currentState.errors.push(stepName + ': ' + reason); handleStepFailure(currentState); } function startNavigationTimer() { clearNavigationTimer(); navigationTimer = setTimeout(function () { if (entryFinished) return; var currentState = getState(state.accountId); var stillOnIndex = String(window.location.pathname || '').indexOf('/PersonWap/Index') >= 0; if (!stillOnIndex || currentState.step !== state.step || !isRunActive(state.accountId) || isPauseRequested(state.accountId)) { entryFinished = true; return; } failEntry(currentState, '【' + examName + '】网页原生入口 25 秒内没有打开考试页,已停止并准备重试', 'native exam navigation timeout'); }, 25000); } function triggerTap(element) { if (typeof mui !== 'undefined' && mui && typeof mui.trigger === 'function') { mui.trigger(element, 'tap'); return; } var tapEvent = document.createEvent('Event'); tapEvent.initEvent('tap', true, true); element.dispatchEvent(tapEvent); } try { saveVars(); dbg('[' + examName + '] 进入方式: 网页原生入口'); log('【' + examName + '】正在打开网页原生试卷列表...'); triggerTap(entryButton); } catch (e) { log('【' + examName + '】网页原生入口触发失败:' + e.message); state.errors.push(stepName + ': native entry trigger failed'); handleStepFailure(state); return false; } waitFor(function () { return !!document.querySelector(entryPlan.listSelector); }, 20000, function (listReady) { if (!listReady) { failEntry(state, '【' + examName + '】网页试卷列表加载超时,已停止并准备重试', 'native paper list timeout'); return; } var paperItems = document.querySelectorAll(entryPlan.listSelector); var paperItem = paperItems[getExamPaperIndex(examType, state, paperItems.length)]; var selectedPaperId = String(paperItem.getAttribute('PaperId') || '').trim(); try { dbg('[' + examName + '] 已触发网页原生试卷项'); log('【' + examName + '】试卷列表已加载,正在由网页建立考试会话...'); triggerTap(paperItem); } catch (e) { failEntry(state, '【' + examName + '】网页试卷入口触发失败:' + e.message, 'native paper trigger failed'); return; } startNavigationTimer(); waitFor(function () { return document.querySelectorAll(entryPlan.unfinishedListSelector).length > 0; }, 20000, function (unfinishedReady) { if (entryFinished) return; if (!unfinishedReady) return; // 已确认进入未完成列表,改由接口自身的 20 秒超时负责,避免旧导航计时器抢先判失败。 clearNavigationTimer(); asyncAjaxRequest({ url: '/ExamManger/P_ExamDetail/GetMyNotExamListOne', type: 'GET', dataType: 'json', data: { type: entryPlan.unfinishedType, rows: 100, page: 1, pid: M_PersonId }, timeoutMs: 20000 }, function (listResult) { if (entryFinished) return; var response = listResult.ok ? listResult.data : null; var rows = response && response.success && response.data && typeof response.data.length === 'number' ? response.data : []; var examDetailId = findUnfinishedExamId(rows, selectedPaperId); if (!examDetailId) { failEntry(state, '【' + examName + '】未完成列表中没有找到当前试卷记录,已停止以避免续接错误试卷', 'matching unfinished exam missing'); return; } var unfinishedItem = findMatchingExamItem( document.querySelectorAll(entryPlan.unfinishedListSelector), examDetailId ); var continueButton = unfinishedItem ? unfinishedItem.querySelector(entryPlan.unfinishedButtonSelector) : null; if (!continueButton) { failEntry(state, '【' + examName + '】找到当前未完成考试,但网页继续考试按钮尚未加载', 'unfinished continue button missing'); return; } log('【' + examName + '】已匹配当前试卷的未完成记录,正在通过网页原生按钮继续...'); startNavigationTimer(); try { continueButton.click(); } catch (e) { failEntry(state, '【' + examName + '】网页继续考试按钮触发失败:' + e.message, 'unfinished continue trigger failed'); } }); }); }); return true; } function handleExamTaskFailure(state, monthlyState, stepName, reason, message) { var accountId = monthlyState ? monthlyState.accountId : state.accountId; if (isPauseRequested(accountId)) { log('【' + stepName + '】当前已暂停,本次不会重试、跳页或交卷。'); return; } if (monthlyState) { failMonthlyExamAction(monthlyState, message); return; } state.errors.push(stepName + ': ' + reason); handleStepFailure(state); } // 在考试页答题 + 交卷 function doExamTask(state) { var monthlyState = arguments[1]; var isMonthlyExam = !!monthlyState; var flowState = isMonthlyExam ? monthlyState : state; var stepName = ''; if (isMonthlyExam) stepName = '月考'; else if (state.step === 3) stepName = '手机考试'; else if (state.step === 4) stepName = '模拟考试'; else stepName = '考试(step=' + state.step + ')'; dbg('[' + stepName + '] 考试页加载中, step=' + state.step); log('【' + stepName + '】考试页加载中,等待题目数据...'); // 等待 allShiTi 加载(题数由服务器决定) // 如果考试页报错(如"考试信息未找到"),立即停止等待 waitFor(function () { if (isExamPageError(typeof vData !== 'undefined' ? vData : null)) return true; return typeof allShiTi !== 'undefined' && allShiTi && allShiTi.length > 0; }, 60000, function (ok) { if (isPauseRequested(flowState.accountId)) { log('【' + stepName + '】已暂停,题目不会提交。'); return; } // 先检查是否考试页报错 if (isExamPageError(typeof vData !== 'undefined' ? vData : null)) { var errMsg = vData.ErrorMsg || vData.errorMsg || ''; log('【' + stepName + '】考试页报错:' + errMsg); handleExamTaskFailure(state, monthlyState, stepName, 'page error', '考试页报错,未执行交卷。'); return; } if (!ok) { log('【' + stepName + '】题目数据加载超时(allShiTi 未加载)'); handleExamTaskFailure(state, monthlyState, stepName, 'allShiTi timeout', '题目数据加载超时,未执行交卷。'); return; } log('【' + stepName + '】题目数据已加载,共 ' + allShiTi.length + ' 题'); loadVars(flowState.accountId); // ========== 答题 ========== // 方法1: 用 move2 + SD 答题(如果 move2 可用) var useSD = (typeof move2 === 'function' && typeof SD === 'function'); if (!useSD) { log('【' + stepName + '】页面答题函数缺失,已停止且不会交卷'); handleExamTaskFailure(state, monthlyState, stepName, 'move2 or SD missing', '页面答题函数缺失,未执行交卷。'); return; } if (useSD) { log('【' + stepName + '】使用 move2+SD 答题...'); var ok_count = 0; var miss = []; for (var i = 1; i <= allShiTi.length; i++) { try { move2(i); var bzda = ''; var bzdaEl = document.getElementById(i + 'bzda'); if (bzdaEl) bzda = bzdaEl.value; else if (allShiTi[i - 1]) { var strs = allShiTi[i - 1].split(','); if (strs.length > 13) bzda = strs[13]; } if (!bzda) { miss.push(i + ':noBzda'); continue; } var sjda = ''; var sjdaEl = document.getElementById(i + 'sjda'); if (sjdaEl) sjda = sjdaEl.value; else if (allShiTi[i - 1]) { var currentFields = allShiTi[i - 1].split(','); if (currentFields.length > 4) sjda = currentFields[4]; } // 检查是否已答对 var match = answersMatch(sjda, bzda); if (match) { var matchedFields = allShiTi[i - 1].split(','); matchedFields[4] = toSubmissionAnswer(bzda); allShiTi[i - 1] = matchedFields.join(','); ok_count++; continue; } // 选中答案 var style; if (bzda === '对' || bzda === '错') style = '3'; else if (bzda.length > 1) style = '2'; else style = '1'; var toggleIndexes = getAnswerToggleIndexes(sjda, bzda); for (var j = 0; j < toggleIndexes.length; j++) { SD(i + '|' + toggleIndexes[j], i, 'False', style, i.toString()); } // 验证 sjda = sjdaEl ? sjdaEl.value : ''; if (!sjda && allShiTi[i - 1]) { var updatedFields = allShiTi[i - 1].split(','); if (updatedFields.length > 4) sjda = updatedFields[4]; } match = answersMatch(sjda, bzda); if (match) { var verifiedFields = allShiTi[i - 1].split(','); verifiedFields[4] = toSubmissionAnswer(bzda); allShiTi[i - 1] = verifiedFields.join(','); ok_count++; } else { miss.push(i + ':' + bzda + '->' + sjda); } } catch (e) { miss.push(i + ':ERR'); } } log('【' + stepName + '】答题完成:正确 ' + ok_count + '/' + allShiTi.length + ' 题'); dbg('[' + stepName + '][页面答题] 正确' + ok_count + '/' + allShiTi.length + (miss.length > 0 ? ', 未答对' + miss.length + '题' : '')); if (miss.length > 0) log('未答对: ' + miss.slice(0, 5).join(', ')); if (ok_count !== allShiTi.length) { log('【' + stepName + '】存在未答对题目,已停止且不会交卷'); handleExamTaskFailure(state, monthlyState, stepName, 'verified incomplete', '存在未答对题目,未执行交卷。'); return; } if (isMonthlyExam && !canSubmitScheduledExamAsFullScore(allShiTi)) { log('【月考】未能确认整卷为满分答案,已停止且不会交卷'); handleExamTaskFailure(state, monthlyState, stepName, 'full score verification failed', '未能确认整卷答案均正确,未执行交卷。'); return; } } setTimeout(function () { if (isPauseRequested(flowState.accountId)) { log('【' + stepName + '】已暂停,答案已保留在页面但不会交卷。'); return; } try { var answerPayload = buildExamAnswerPayload(allShiTi); if (!answerPayload) { log('【' + stepName + '】试卷数据字段不完整,已停止且不会交卷'); handleExamTaskFailure(state, monthlyState, stepName, 'invalid answer payload', '试卷数据字段不完整,未执行交卷。'); return; } var savedKsmxid = ''; try { savedKsmxid = localStorage.getItem('bmzhkt_exam_ksmxid') || ''; } catch (e) { } var currentExamId = resolveExamKsmxid(typeof ksmxid !== 'undefined' ? ksmxid : '', savedKsmxid); if (!currentExamId) { log('【' + stepName + '】考试记录标识为空,已停止且不会交卷'); handleExamTaskFailure(state, monthlyState, stepName, 'ksmxid missing', '考试会话缺失,未执行交卷。'); return; } if (isMonthlyExam) { submitMonthlyExamAnswers(monthlyState, currentExamId, answerPayload); } else { var examType = state.step === 3 ? 'mobile' : 'simulate'; submitExamAnswers(state, stepName, examType, currentExamId, answerPayload, '页面交卷'); } } catch (e) { log('【' + stepName + '】页面函数交卷异常: ' + e.message); handleExamTaskFailure(state, monthlyState, stepName, 'submit exception', '页面处理异常,未执行交卷。'); return; } }, 0); }); } // 考试结束,更新状态并跳转 function finishExam(state, stepName) { dbg('[完成] ' + stepName + ', 返回首页同步该项目积分'); state.step = getWorkflowSummary(state).nextStep; state.stepRetries = 0; saveState(state); log('【' + stepName + '】任务结束,跳转首页...'); setTimeout(function () { if (!isPauseRequested(state.accountId)) location.href = getIndexUrl(); }, 3000); } // ========== 显示最终结果 ========== function showResults(state) { var summary = getWorkflowSummary(state); var total = summary.total; var limits = normalizeDailyScoreLimits(state.dailyLimits); var targetTotal = getDailyTaskPlan(state).targetTotal; var skippedHtml = ''; // 旧版本可能留下跳过记录;新版完成条件只看五项任务积分,不再展示过期记录。 var skipped = summary.completed ? [] : (state.skippedTasks || []); for (var i = 0; i < skipped.length; i++) { skippedHtml += '
已跳过:' + escapeHtml(getTaskLabel(skipped[i].id)) + '(' + escapeHtml(skipped[i].reason) + ')
'; } var title = summary.allSucceeded ? '每日任务已完成' : '任务尚未全部完成'; var titleColor = summary.allSucceeded ? '#4caf50' : '#f57c00'; var serverTotalHtml = isFinite(Number(state.serverDailyTotal)) ? '
服务器今日总积分:' + Number(state.serverDailyTotal) + ' 分
' : '
服务器今日总积分:未取得有效响应
'; var html = '
' + '彬煤智慧课堂 ' + title + '
' + '
' + '
登录签到:' + state.signInScore + ' / ' + limits.signInScore + ' 分
' + '
知识学习:' + state.articleScore + ' / ' + limits.articleScore + ' 分
' + '
手机练习:' + state.practiceScore + ' / ' + limits.practiceScore + ' 分
' + '
手机考试:' + state.mobileExamScore + ' / ' + limits.mobileExamScore + ' 分
' + '
模拟考试:' + state.simulateScore + ' / ' + limits.simulateScore + ' 分
' + '
' + '服务器五项任务积分:' + total + ' / ' + targetTotal + ' 分(' + (summary.reachedMinimum ? '已学满' : '尚未学满') + ')
' + serverTotalHtml + (skippedHtml ? '
' + skippedHtml + '
' : '') + '
点击此面板关闭
' + '
'; var panel = document.createElement('div'); panel.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);' + 'z-index:999999;background:#fff;border-radius:8px;' + 'box-shadow:0 4px 20px rgba(0,0,0,0.3);min-width:300px;max-width:90vw;'; panel.innerHTML = html; panel.onclick = function () { if (panel.parentNode) panel.parentNode.removeChild(panel); }; document.body.appendChild(panel); // 同时保留浮层 dbg('[最终结果] 签到=' + state.signInScore + ', 文章=' + state.articleScore + ', 练习=' + state.practiceScore + ', 手机考试=' + state.mobileExamScore + ', 模拟考试=' + state.simulateScore + ', 五项任务总分=' + total + '/' + targetTotal + ', skipped=' + JSON.stringify(skipped)); log(title + ',服务器五项任务积分 ' + total + '/' + targetTotal + ' 分。'); if (skipped.length > 0) log('有 ' + skipped.length + ' 个项目已跳过,原因见结果面板和日志。'); } // ========== 卡密验证模块 ========== // 说明:脚本只保存卡密的 SHA-256 指纹(不可逆),不保存明文卡密。 // 用户首次运行需输入卡密,验证通过后写入 localStorage 永久激活,之后不再弹窗。 // 如需换卡/重新验证,在浏览器控制台执行: localStorage.removeItem('bmzhkt_license') // // 卡密格式: BMZHKT-XXXX-XXXX-XXXX (去掉了 0 1 I O 等易混淆字符) // 使用浏览器原生 crypto.subtle.digest 计算 SHA-256,标准 API 不依赖外部库 var _enc = 'UFhMXVcICwRUXUlTBQdRU1MMHlIECANVUFRIDwcGBgFSCUNSUwgCVAQOQgoHBwEFVwgeWQVRUVUGXhhbBwJTDx5dQwgDBAtQVwhJXFEHAlJUXxhaBQRWUgRVTFoLUlcCU1lDDQdSAQdVWx5YA1EGBAMPQ1lRU1BXBlQYWgQGBgdbEU5SAlQEAAdbGFlWUQoEUFkZU1QAVAVWC0lTAgcKAwQPSw0AUlYFVVxODgsIAwQADE9cUFIADwFbGVIKCAIHBAsGDVYACgVWXBwJCglQU1ILSlwLCAJVBl1ICQcDAg8DXUoOV1ZQVAFYGFsBCQtXA19IDVNSBFNQD05cVwZRAwZdSxcEVQQGV1pJX1NSAFRbXE8PVlVWVQQLTg4CCVQAVghNX1ABAg9aD09aV1VUBlcPHl8ABlACV1VKCgYGB1RVVRgJTggDBlZZTA4KAQYFUwxPWgUDCgNWVU5YCgdTVFYIQ14FBAAPUlxDWgoBBVIHCE5eUwZQAlEPHw0ABAAOAFVDWVdMVgdWXkxZUAALBFIPTQ4AUlQGWw4bXlEEA1NRWUxbUQhWDwAOQ1kHCAoEU19NWgcHCwFTXEtcBVFWDgZYSlkDUU4HVlRPUgUHCgAGC0kIC1UHUlYPGF8BUlMOVFsbXwUEAQEDWU8OBFIEVAcMHFsDB1ABUAgYUwEACgVTWU0NAQMBSlJdHF5TUQpQBghDCgADV1VbX0lZCgUCUAMMG10KAwoEBlwZCVFWCgFTVEINB1QKVwFYHw1QAlcPVQtKCQMAAQ4eCUwIA1QFBldVTAoBAgBSW14fXVAEV1ABD0tSCgYHDwNaGVxXBwQHUg8cXwJRBANTXRkIAwEEBwZcGVoAUwZVAREbCVAAAQ9VWhteUQZQAQQMHA1RAwIOVF4ZWlBSVAYGWUxaVwcGAANbTl0FBQBUBglKUgpUVwNSCR5eVAFQUgFcBg9RAgFXWlwfWFdWAgBWWRtZBARQVQReSloHAQYEUFwZCgVSCgRSVUoJAwEKVVoIQg0DCFNXB1hDWAFWV1dUXUIXC1JQDwZdTQlUVAAAVAkZDlFWBwMDXBxbAwdWA1FdGFMBBQAHW1keDgRRVg4EXxtbUFUABgcLGw4BVgECW1pMCU4HVwNWD0sOUQQLBAZZTQhRCQAEAF1PXFYAV1BUXkwIAgIFVANfSg0LBQcOBAxKUgoICwNaWEJaUAIBUwNbT18HTAMCVAseXwACBgRTVUNbVgZUVwBdTlxQU1cEAAlODwNWCwFUC0tTUQRRVVoIHwlRAAIHBwlLXldSBlRTX00NAQhOUgZeT1NXBAZTU1weDwMJAA9RWBgJAwADAFMLSFoGBwoGB19DWVcFVwJUWhwNVwJUBFYLQ1sHVVECW19PWQsCA0pQVUhaCgZRAAAJSg1RUgIAB1lICAQDA1JTVBwJAVUEBQBcTw0BVAFUUg9MCQRTAQ9SXEleAVQEUlZZQ1gKCAQGHgwfCQdUCgVRC0JdVlMFAFNcTFtRBARQBwlLDQEEBlIAWBhYAlMFUFpYQggABgAPBg9KCgEAAgUDWBlaAFUBUgcRHF1RAQUCVlRKCVRRAFVUX0xYUFJRBgZYQglXAAEDV1xOClACBQJVXUwNUQNXUlZdHFJQUQYHUF5PU1dWAlMHWQYPAVQDUFBeHAoKAgMEBwhDClFRAARTDkpaCgcBA1oPSQ5UVQsFWw5NCQcDBFIBD0wIBVVTAwNbTQ1XAgRUVFlPFwAEBg8ECBwOVFUKUgMJTVpRBARQW18cWVRSVAdRDBheVgIEVAQOSFNXAAtUV1gfXlNUV1QADkwKCwMBBgBYTglOAlQAVwtMCVBTUVMHWU1aAlVRDwEOTVkBVgJXVA4YCgsGAw4DXRxYBlMGBlsMSVMCUVZTUAhMD1MCVgJRXkhTUUwDA1VeTQgLAQBUVVRCWQcBAFJUVExTAANQAQcPSFMFUwsPUlRMWlMHAQQAXh5aClVRB1QMSg5RBQcHUggcX1MFTlVUCBxSAFYAAgcIGFMAAVYAUlVCXAYIUwFaVUtfUAIFBlBaSAoDBQAFVQxMDwsGCwFSXRhdUQZRBQYJHA8HVQBKWl8YCVQABFAAW0NTAgEEU1RZSAlQBwcHBlVDDwcJB1BXCUsPA1MLDwBcSw0LCQUAWlwYXlEHC1VbCxhYAAAEBR5UQlgABldVAVxMU1YJAQUGWB5TVgBQD1QPSAoLCQoGAwgfWQUIUFBXWEkPB1QKBARdSwkDAQQFUAxCWwcDA1ABEUsPU1EDBABYSQkKBAMEVVxNDgMBAAJTW0NaBgZTBgZcHwpWAlQEA1lKWlQABA8GDxlcBAMHDwRZSF8HVVRXVV8GCQECAQUDX0oNAQEGAFQOHw1RVlYCUlRDDwQCBFcBWkxeBwUAVAMOGF0CBgBQUl4fCARVV1JVXUwNCgFXVVcOTRcEAAQOUAxOCgEIClQEXUgIUwRUB1daTlwACQBTAFtOXAECUQEDCEkOUQRTBVVUTFIAAVMEUVgeXVcJA1RaW04NTgYKUANdQghTUQcEUFUYWwYABwIED0IIBgIDBFIPGFgACVMBAQ4bXwMEU1AGDE8OAlELUAFcT1kEUwtQVlxDXgpMBgFbVUsNBAEHVVNaSlxRAFRSVFtDWFAIVgcHVUIJCwABAlRaHgpTVAEGUA4YDQFSUwFUXE5SVAMCVVVZSFkLAk4EVVxLCQIHBAZaDxteBgMABVNUTltWAwJSVVkeWwUHUVdbWh9ZV1QGAQMITw1UAAcBVlkYXVdSUwZRCEhaV1UFSltUHl8CBlRSV1xIWwFSAgVbX0IJAglXVFBUSl1TUgsPA1hDDVZSVgYEVExTB1NQAQYPSghTVQQEBltLClRSAgQeXEpYAVZWBVQPQ1gAVAoBA15PWAUEBgFXVBlaAVQFU1NfTQ1QCFAGAwkZXgUHBQNSD09TBQMGBlMJGw1WUwRTVxFNX1BTVwRUXk4PCwdUUFQJT1tRBFNSVV1KW1dVB1AADx4PAgMCBwAJGQ5QA1YFBlRCUgYGVFMGDk8NCwgKAlYMBlIDAgpXUF8bXwNWUwAHXRlZUwQBDlRYTg8HAwABB1xOCgYICwABXBkICgZQUlsJHlIDAQpXVV5LDgAEB1cDDBgXBFNXAgELTQ8AVFQFAV1OW1cAVAZaXksKAFYHVwEOSQkDAVACB10eDVRTBgEHDxlfBwAKVVJfQw1UAQpSBFhOWU4JCgVTXkwNBwNXBVtcHlgHBwIGW1lIXgMDVAZaXExYVAlTVAFUSQhTAwpTBFpOX1NSCgUACEpdBgRQBQdVHglXTFAGUVgZXQFSBQRUCBxaAVZRBltcSloHUwcOVFobUgsDAg4HXBtTAlZWUFYLHFkGBAtUVg9KWFRUUwFRVExeBQZOAVUMT1xUAAIPBwlNWVFUAQNRXkNTC1UDAwBfH1tQVgIGBFUeCAYIVgEDWxlYU1IGVQNcTVoHBwAFBglNUgJSBEpUWRsIBQYCAFtbTF1XUgAAUA4YUgYIBVBTXE4PUwcDVQYLSl4BAFYHAAwfDwJVAgUBDEldVwBRVQYPTlNQVAUAHggeCgIBAAFQXxgKUAQCVQMPGVMFAVEGUwwZUwMJBFMHXUxdUQcFAQdZGFwKU1FSUgwcUgBWVw5bVEtYUwkKVwYRTFpRBVFTAQkYXAoJV1NUXh4OClRRBVBUSQ9QVABQUFRNUlRUVwADDk9bBQYHUgYMHloBVgdVUFwfCQAEUAQEVAZYVlRTBFpaQl8FUgEOB19KUgBVAFBQC05fAQcLDlsLSQhTUgJQAQ8cDgMDUQdaXU8NCwkDB1UJS1tWUlNVWg9KF1QECwYDVUsKUAAAAFNdSFxQVAAAAFkeWANUCwdXCRkJVwYAAQEIGF1TVQEGB1wZWAADBQBaWxkOB1IKAVVUGVJOUQsAWw8ZXAAHBg4AWkhcBQUGUFRdSl1UCQNSVgwYXFRRBwdSWE9eAAYLB1sLHFpUVQJVU1keXARWC1NbDk5bVEwLBwYJHw5XAAJVVQtPXwNTBwQBWxlYVgYCAVZeTVtWUgIOWwtCXwIHVgNbCUwNBgdRAFZZGVJXCQNSVl0YDlFRTlUHDk4PC1QKB1YISlxTAgRSVVQcXQMGA1JWDxtYAQALVARUQlxUBQcBVF4YDgQDCw9bW01SUVNWA1NUGF4CCABKVwlLWVBUBFdTC0MNAwQBAQRaH1sBCQtTVFgeW1AGUAFQCRhZC1ECA1tUGQpUBVEAV1RNCFQJBQcHVBtcUFRTAR5ZHwoCUwVXWwhJXwVVUQQHXE4IBlYDBQRfGQ5XUQIFUVgYXgIFVFMDXUIPB1ZXBwcPS1JTBQEBAQhKXQNTC1UBEUMNVlFQAwdfQlsCVlBTUVUfWwVRAFVSWU9bV1QFUFJfTg4GUwYGAA5KWgsAVw5RXR9SAQALV1ddS1MHCAQGAw4GXwJRUFdWCxkNV1ZTAFBZSA4BAVMHAQtLXFECBVAGVRxcBQEEUltVGwgGCFAPBgwfXQEBBVVVDEpZAgBRAlQOGRdWVlNVAFobCgEDAAQHXUleBAgGBFZbG1JXBFcOUFtLCVBVUAdUVEhcBANTAwdaH1gHVQIFUFwZWlQAVAUHXkJcTgRXVVRZH14EVQdTWlwZXAEACg8ADkpYBwZXVQdbT14HUlZSVA9CCFZTUQ5TVEpaB1FTVVNcSF0EVFNVUg8ZXQZMAVcDWEoIBwFWUFUPHlgBAwsGWg5OWANSCg9TVB8NVAcFBlRbGwlRCQACU1gZUwtRBwZTXh5cCwYFV1AOG1kEUU4EBllDUwAEBwMAXx9SBVQHUwZaTVNXVVQCBlsbXlEJBlIHD0NZAwULDwZVHF5UAgVXUQhPCgJVUQUBC0hfCgcBSloJSghRBVQAW1QeUlAACwAEWkJZAAFUBFVeG1pXCFBXV10bXVFTUQdXWEkNUFNRB1MMSgkKBgdUWgkYDgEAUAQeVEIJAQgEBFVYSlsFCQAGBAsZXwVVAg5RW05ZClVXAltcSQgEAgUDBgwfCFYBVgBUWElfVgQEU1ZfG1sFCFFVUhEZDVMHUwcAXEwIBVVRUlQJS14DVlcEVVRKCFABBgBQVE5bAwgBBFFYTVgEBAIBA18cXFZUBAMEC0IIBAQFAVoLBghXUgICBF4cWlAEUAZRVR5aCglUBVBeTFsDUwtUAw5IX1dSAA4DDklbB1MGAFNbSlIHCFAGAA9CWQEHAlABXk8XA1EGVVtaHlpUAQNSAF5DCQpSUVIGCxlSBQUEB1ZcGwoFBwoBUV1IUwJVB1RUC04NAVYAV1APT1tQAgEEB11DCU5SC1RbXRsNAwBQUlFbT1pQUwUHBwkcXQIEAwUGXExYBFIGA1ZcTQlTAQJUVglIDwZVCwFVX0wNBVIKAQBUSQ8KTAJQUFsZXFMJBwYHDkgJC1NXBFQOGQ8EBwYPB11CUgcBUQUEVUkOAgUGBAAPT1wBUQVSVAseCQcBVg9XCUsJBQNOAFFcGVoGCQMFUl1OCFEHAwFTXEJaAVJRAlteHg0CAgVSUltODlADBVIHDkpTVgAGU1sIH1lUCQUDUF9JU1ZWC0oBW0sKVghTD1dfSw4HBwUDV1tICAEDBQBRXklSBQcEA1NfQlgDVAoFUA8fUwEAV1VbCB9dBFILUlNdS1xQAlFTHl5DCQpVV1dVXBsIBQUHV1pUSw4ABwUGAVpLCgMIBA5bCx9TAQYCAQQPHg4BVFEHBF8bDQEGAARXDxxSC1JQUlQRTV1RBgICB1keD1YBUFcGVB4PA1JRAFdaQl8LBQEDBltNWwQDBg4BDxhaUwRUV1ZZH1pWCFMBV1lMCARWCwUHCQZfClFRBVcMSQpTAwFSAV9CWVMDVA9TWUJYVwUFB1ZUTggBCFFUUAscWApWBABVDksKU1MAAAdfTA0KBldSA1lOF1QAVARTD01aAVEDV1ZUQlsECQQCWlRIUlcCVAFaD0xSBQdTA1ZaSg0FVFMCWlhICQJVBwNWVUxeCwcFAVRZSFtOBVYBBA4YD1MBVlNVCRtcUQEBBVNcHloLAFEPVFUfWwEJUQNVXE1cBgNUDlYPGAoFVFACUFkcD1AHV1RTWBgIU0wKVVVbHwkGVlNUAVgZWQAABFIDWxlaAwAFAFcLTlpQBgFUUAtDWQUEVAVUCUpfBAdXAVVVTVsLVgoAVw9PXQsGTlNbXx9SUQQDBVJUTQlXUgMDVF9CDVZUC1dXX0wIBQhQDloITwoKBVEAAQ9MUlcAUAZVWEINUQQHBFdcQlIABFRKBltMDQFTUwRVXhhfBggGVVVUHFlWBlNVUA4YDgRRA1AGXkxeBAZQUgNVH1MHCVcCUA5KWgRTBABRWh8PAVYKVB4PHlIKAgpXW1tJCQMCCgBTDkMPCwFWBFBYHgkKUgsOWlVMDgUICwcGWENfCwMDBgBdHg4LVQEPBw5MCFBWAQUGEUgIAVRUVFdUQw5WUgECU1RNXFcDCwRQVR9aVlMBUlFYSg1TUlYBAVQfWgYBBg5QDk0NVwdRD1FVQ14AAAVSUlwGCVcFBlRaDEJSUAYEAVJbGFxXUlZSA1VLCQIAVFMGWklTAAkEAlMJSwoHUlZSAAxJXAABBVIHWRxbAlICBlUOSxdWCAUDUV5ODgYGClIBDxhfCwMDV1FfG1wKUQVVA18cD1ZVBQJSWk4KVAYBUFJcHgkKAwRUBl5JWgEEUwRTWx9bTlNRBFpZG1MDVARUVVxIXgJWCwdWWU1YCgQCUABeTAkBCVAAAAtKUwEDCwRVDkMOVwhWVVpfQ1IDUwoEB11CX1NMVgFXXUhZAgZUUgMIQlsCVgoFUVhJXwoFVlcAXhtdAQMGAlVYG1tRCQBUV11CW1ECBFJUVU5eVFIGUwBcGVIDBU5SWglMDQEJBAJXCRxSUAQABFMOTQ4GAgNQAFhLXAoBAg9TVBwIAVEDBQFVTwkLAAUEBAhDWAYGC1NQWh8OBQQESgNVGFlXCAoGVwgcXAUFBQVTXklcA1NTA1tbHwkEVQpUUlpKClFVBwRSW0kPUFFQBFdfSQgKCFMPUV8ZXAsHUwAeD0oJAFEABVNVS1gFUQYGAwlMCAUBCgEDXElSBwELBVZfSg0DUgUEVVpDXgtWU1RSVEhSVwBXBQEITw4AUldSVRFJWlAGBgYAVBlcClYCD1BdQwkLUlEPA1QeCAZVAwYGWEtcAlQBBVtdSVICAAsOBl5NDQUFAlMEXxtbBwhXDwRVBlIDBwFXW1UcWQQHC1NQXB5TVwZRAQFeTA1QAgEHBwgfXQVRCw8DD0pTUQUHAgdbSwoAUVdTVVhICABVUAVbW0kXAQICBQNdG1lRBFAHVg8cCFMFUQVQXxwIBQkHVFpaSwhWUgYHUAhCU1RRAVIBCRlTCwlXVVJcGFkHBQYOVQ9CWU5WBlNWX05fBFUAA1NbSApWBgZQVVRKDVEJBgUHCRxfA1ELA1oOT1JXB1QGVA5DDwoJCwRbXRxaCwQLUlBUQwkBTAJUU1kYWAEDBVUGXU1fVgEEAVoMSV0FAgQDUgxCDVBUUw5SVBhfUQAGVFYIG14HBQVSAwscCQMFBQ4BW0NYV1ZOVFFeQgoGBgMBVVlOCFBWVFBUVUteBwMLAlQIGQ1TVlcHVFpPClZVVgZbDBxdAFYGAFJdQ1MDB1MGAVpJXQcEUEpSDxheBwgLV1VUG10LUlMPVFoZWwMAVgVVXx5cA1ULBlpUTVhUVFMDBg4cCVBVVg4BXxxeBggHBVRbSwkGVQoOHgtMWVEICgJWW01dUwkGAAdZTFoEVAIOUAgbWwAJAQ9TVUleBlJWUFRfH1gFAgMAAV1PXwIHUVNUWUxfUFZUVVoRSQ9XAAoGAVRJXVFUUFAHXUtYVlIEBVtaQgoBAgdQAQsYDgcFCgVbC0NdAVUCAlpeGFhQVgNSB11OWFcDBFJRXAZSVgICAlZfTwgGCQQEWllMCgMDVlVRXEtdU1IFAFsJSQoHVQIFUl9LXlNTV1NTXU4KAQQDVVQMSFhWA1ABUFhMFwcHAwBUVBxYUAAADlFaT1sCBQIBUF1DCQIGBA5aDkNeAlVQBAdYTwhWUgBQB1QfX1ABAwJaVUJfBwdRDgFdGFxOA1ZVVwhNUgpTAA5TXUhdU1EEBVRaTQ8GAQtUAw9PCQsCUwBbWE9TUAkDD1ULHglRBwNSBlpJWgdRBQMHCBlfBUxRAAddSlwDVQUEBgxLDgdUVFRSXRgPAlUCUwZVQlJQBFEABF8cD1ZTAAVWCR8NAQkKBFBbH1wBBAMHAFhDClcCTgJbCUpYU1QLVVUJTVgLBQEDVF9LWwcHVlQADksIVgADVVddQg4GAgcOVV4fXgsHUQFbC0NfU1QGBlddTQ0AVgdKUlpCDQABVFAECRlbBwYBD1daHlxWA1EEVQlNUgJTCgJWXhhSBlRQUABdG1oFBgtUUghMW1NSBFNXVRleVgVWAx5ZQ1tWBwBTUFkfWQQAUFRRWEsKVwcEAQcOQw1RAgEFU1hODwpRVlQGWkwOC1IEUFVbHl5WBwYGVV4bD1dVVlcGEUtZA1IGVAcISVsBU1BSAFpDXgIDCwJXCE9eUQEGBloLTllTVQEHBghNWwQCB1ABDkwNVFICAAZaT14ABAABAQ4GUwFSVFJVWUlZUAMDVVdVS18LVAYDAFpIWgQBVgMAWB5fClMGA1RcHwgHBQFTUw9IU1FSVFAGWUJSAgUKA1UPGw=='; var _xk = 'bmzk2026'; function _dec(enc, key) { var raw = atob(enc); var result = ''; for (var i = 0; i < raw.length; i++) { result += String.fromCharCode(raw.charCodeAt(i) ^ key.charCodeAt(i % key.length)); } return result; } var ALLOWED_KEY_HASHES = _dec(_enc, _xk).split('|'); var LICENSE_KEY = 'bmzhkt_license'; // 保存已验证卡密的 SHA-256,不保存明文 // HTTP 页面没有 crypto.subtle 时使用的本地 SHA-256 实现。 function sha256Fallback(text) { var K = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ]; var H = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]; var utf8 = unescape(encodeURIComponent(String(text))); var bytes = []; for (var i = 0; i < utf8.length; i++) bytes.push(utf8.charCodeAt(i) & 0xff); var bitLength = bytes.length * 8; bytes.push(0x80); while (bytes.length % 64 !== 56) bytes.push(0); var high = Math.floor(bitLength / 0x100000000); var low = bitLength >>> 0; bytes.push((high >>> 24) & 0xff, (high >>> 16) & 0xff, (high >>> 8) & 0xff, high & 0xff); bytes.push((low >>> 24) & 0xff, (low >>> 16) & 0xff, (low >>> 8) & 0xff, low & 0xff); function rotateRight(value, amount) { return (value >>> amount) | (value << (32 - amount)); } for (var offset = 0; offset < bytes.length; offset += 64) { var w = new Array(64); for (i = 0; i < 16; i++) { var p = offset + i * 4; w[i] = ((bytes[p] << 24) | (bytes[p + 1] << 16) | (bytes[p + 2] << 8) | bytes[p + 3]) >>> 0; } for (i = 16; i < 64; i++) { var s0 = rotateRight(w[i - 15], 7) ^ rotateRight(w[i - 15], 18) ^ (w[i - 15] >>> 3); var s1 = rotateRight(w[i - 2], 17) ^ rotateRight(w[i - 2], 19) ^ (w[i - 2] >>> 10); w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0; } var a = H[0], b = H[1], c = H[2], d = H[3]; var e = H[4], f = H[5], g = H[6], h = H[7]; for (i = 0; i < 64; i++) { var bigS1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25); var choice = (e & f) ^ (~e & g); var temp1 = (h + bigS1 + choice + K[i] + w[i]) >>> 0; var bigS0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22); var majority = (a & b) ^ (a & c) ^ (b & c); var temp2 = (bigS0 + majority) >>> 0; h = g; g = f; f = e; e = (d + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 0; } H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0; H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0; H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0; H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0; } return H.map(function (value) { return ('00000000' + value.toString(16)).slice(-8); }).join(''); } // HTTPS 优先使用浏览器原生 API;HTTP 和旧浏览器使用本地实现。 function sha256Hex(text, cb) { var done = typeof cb === 'function' ? cb : function () { }; try { if (typeof crypto !== 'undefined' && crypto.subtle && typeof TextEncoder !== 'undefined') { var enc = new TextEncoder().encode(text); return crypto.subtle.digest('SHA-256', enc).then(function (buf) { var arr = Array.prototype.slice.call(new Uint8Array(buf)); done(arr.map(function (b) { return ('0' + b.toString(16)).slice(-2); }).join('')); }, function () { done(sha256Fallback(text)); }); } } catch (e) { } done(sha256Fallback(text)); } // 归一化卡密:大写、去空格 function normalizeKey(input) { return String(input || '').toUpperCase().replace(/\s/g, ''); } // 校验卡密(异步):算指纹并与列表比对 function verifyLicense(input, cb) { var key = normalizeKey(input); if (!key) { cb(false); return; } try { sha256Hex(key, function (hash) { for (var i = 0; i < ALLOWED_KEY_HASHES.length; i++) { if (ALLOWED_KEY_HASHES[i] === hash) { cb(true, hash); return; } } cb(false); }); } catch (e) { cb(false); } } function isStoredLicenseValid(hash) { return typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash) && ALLOWED_KEY_HASHES.indexOf(hash) >= 0; } function getStoredActivation() { try { return localStorage.getItem(LICENSE_KEY) || ''; } catch (e) { return ''; } } function saveActivation(hash) { try { localStorage.setItem(LICENSE_KEY, hash); } catch (e) { /* 忽略 */ } } // 自绘卡密输入弹窗(不依赖 confirm/prompt) function promptLicenseKey(onResult) { var mask = document.createElement('div'); mask.id = 'bmzhkt_license_mask'; mask.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;' + 'background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;'; var box = document.createElement('div'); box.style.cssText = 'background:#fff;color:#222;padding:24px 28px;border-radius:12px;' + 'width:420px;max-width:90vw;font-family:sans-serif;box-shadow:0 10px 40px rgba(0,0,0,0.4);' + 'box-sizing:border-box;text-align:center;'; box.innerHTML = '
彬煤自动积分 - 卡密验证
' + '
首次使用请输入卡密激活,验证通过后永久有效。
' + '' + '
' + ''; mask.appendChild(box); document.body.appendChild(mask); var inputEl = document.getElementById('bmzhkt_license_input'); var btnEl = document.getElementById('bmzhkt_license_btn'); var errEl = document.getElementById('bmzhkt_license_err'); inputEl.focus(); function done(ok) { if (mask.parentNode) mask.parentNode.removeChild(mask); onResult(ok); } function submit() { var v = inputEl.value; errEl.textContent = '校验中...'; verifyLicense(v, function (ok, hash) { if (ok) { saveActivation(hash); errEl.textContent = ''; done(true); } else { errEl.textContent = '卡密不正确,请检查后重新输入'; inputEl.select(); } }); } btnEl.addEventListener('click', submit); inputEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') submit(); }); } // 卡密入口校验:未激活则弹窗,激活后才执行 onPass function requireLicense(onPass) { if (isStoredLicenseValid(getStoredActivation())) { onPass(); return; } showTip('首次使用,请输入卡密激活'); promptLicenseKey(function (ok) { if (ok) { showTip('卡密验证通过,已激活'); setTimeout(onPass, 800); } else { showTip('未验证卡密,脚本不运行'); } }); } function getScoreLimitAction(step, studyAcc, studyMax, exerciseAcc, exerciseMax) { if (step === 1 && studyMax > 0 && studyAcc >= studyMax) return 'article'; if (step >= 2 && step <= 5 && exerciseMax > 0 && exerciseAcc >= exerciseMax) return 'exercise'; return ''; } function applyScoreLimits(state, studyAcc, studyMax, exerciseAcc, exerciseMax) { var result = state; var articleReached = studyMax > 0 && studyAcc >= studyMax; var exerciseReached = exerciseMax > 0 && exerciseAcc >= exerciseMax; if (articleReached) { result.articleLimitReached = true; } if (exerciseReached) { result.exerciseLimitReached = true; } if (articleReached || exerciseReached) { result.step = getWorkflowSummary(result).nextStep; result.stepRetries = 0; } return result; } // 检查当前日期的积分上限;服务器首页变量明确达到上限时按已完成处理。 function skipLimitReachedTasks(state) { var vars = loadVars(state.accountId); var studyAcc = Number(typeof M_StudyTimeAccumulate !== 'undefined' ? M_StudyTimeAccumulate : (vars.M_StudyTimeAccumulate || 0)); var studyMax = Number(typeof M_StudyTimeMaxAccumulate !== 'undefined' ? M_StudyTimeMaxAccumulate : (vars.M_StudyTimeMaxAccumulate || 5)); var exAcc = Number(typeof M_MobileExercisesAccumulate !== 'undefined' ? M_MobileExercisesAccumulate : (vars.M_MobileExercisesAccumulate || 0)); var exMax = Number(typeof M_MobileExercisesMaxAccumulate !== 'undefined' ? M_MobileExercisesMaxAccumulate : (vars.M_MobileExercisesMaxAccumulate || 20)); var action = getScoreLimitAction(state.step, studyAcc, studyMax, exAcc, exMax); var articleReached = action === 'article'; var exerciseReached = action === 'exercise' || (articleReached && getScoreLimitAction(2, studyAcc, studyMax, exAcc, exMax) === 'exercise'); // 读文章(StudyTime),检查 M_StudyTimeAccumulate 是否已达上限 if (articleReached) { log('【读文章】服务器确认当天积分已达上限(' + studyAcc + '/' + studyMax + '),本项按完成处理'); dbg('[完成] 读文章已达上限 ' + studyAcc + '/' + studyMax); } // 练习+考试(MobileExercises),检查 M_MobileExercisesAccumulate 是否已达上限 if (exerciseReached) { var taskLabel = state.step === 2 ? '练习题' : (state.step === 3 ? '手机考试' : '模拟考试'); log('【' + taskLabel + '】服务器确认当天练习+考试积分已达上限(' + exAcc + '/' + exMax + '),相关项目按完成处理'); dbg('[完成] 练习+考试已达上限 ' + exAcc + '/' + exMax + ', step=' + state.step); } if (articleReached || exerciseReached) { applyScoreLimits(state, studyAcc, studyMax, exAcc, exMax); saveState(state); } } function reconcilePendingOnIndex(state, onDone) { if (!state.pendingWrite) { onDone(true); return; } var vars = loadVars(state.accountId); getServerDailyTotal(state.accountId, function (dailyTotal) { var snapshot = { dailyTotal: dailyTotal, studyAcc: Number(typeof M_StudyTimeAccumulate !== 'undefined' ? M_StudyTimeAccumulate : vars.M_StudyTimeAccumulate), exerciseAcc: Number(typeof M_MobileExercisesAccumulate !== 'undefined' ? M_MobileExercisesAccumulate : vars.M_MobileExercisesAccumulate) }; if (isFinite(Number(dailyTotal))) state.serverDailyTotal = Number(dailyTotal); var action = reconcilePendingWrite(state.pendingWrite, snapshot); if (action === 'confirmed') { var kind = state.pendingWrite.kind; applyConfirmedPendingWrite(state); saveState(state); log('服务器积分已增长,上次超时的“' + kind + '”已核实成功,继续后续任务。'); onDone(true); return; } setRunActive(false, state.accountId); releaseExecutionLock(state.accountId); if (confirm('上次提交超时,刚才核对服务器积分仍未发现增长。\n只有确认该项目确实没有到账时,才点“确定”允许重试;点“取消”会继续保留待核实记录。')) { state.pendingWrite = null; state.errors.push('pending write manually confirmed not credited'); saveState(state); setRunActive(true, state.accountId); if (!startLockHeartbeat(state.accountId)) { log('执行锁已被其他标签页接管,本页没有重试。'); onDone(false); return; } onDone(true); return; } log('待核实记录已保留,本页没有重复提交。'); saveState(state); onDone(false); }); } // ========== 主流程 ========== function main() { var url = window.location.href; var accountId = getCurrentAccountId(); var state = getState(accountId); var summary = getWorkflowSummary(state); var monthlyExamState = getMonthlyExamState(accountId); var pauseAccountId = accountId || monthlyExamState.accountId; dbg('脚本启动, 版本=2.1.1, URL=' + sanitizeDebugText(url)); // 暂停必须先于月考和每日任务分支,保证两个流程都不会继续推进。 if (isPauseRequested(pauseAccountId)) { bmPaused = true; renderPauseButton(pauseAccountId); log('已暂停。点面板上的“继续”后会重新核对进度。'); return; } bmPaused = false; if (monthlyExamState.active) { runMonthlyExamFlow(monthlyExamState); return; } // 未点击“开始”时只保留控制面板,不执行、跳转或验证卡密。 if (!isRunActive(accountId)) { log('任务未启动,请点击面板上的“开始”按钮。'); return; } // step=7 表示当天结果已经展示过;再次运行需由“开始”按钮确认重置。 if (state.step === 7) { setRunActive(false, state.accountId); releaseExecutionLock(state.accountId); log('今天的任务结果已显示,如需重跑请再次点击“开始”。'); return; } if (summary.completed || state.step === 6) { showResults(state); state.step = 7; saveState(state); setRunActive(false, state.accountId); releaseExecutionLock(state.accountId); return; } // 判断页面类型 var isIndex = url.indexOf('/PersonWap/Index') >= 0 && url.match(/\/PersonWap\/Index\d+/); var isPractice = url.indexOf('/P_ExamDetail/OnlineStuday') >= 0; var isExam = url.indexOf('/P_ExamDetail/OnlineTest') >= 0; // 同时匹配 OnlineTest 和 OnlineTestOne dbg('页面加载: ' + (isIndex ? '首页' : isPractice ? '练习页' : isExam ? '考试页' : '其他') + ', step=' + state.step + ', errors数=' + (state.errors ? state.errors.length : 0)); if (isIndex) { // ===== 首页 ===== log('首页加载完成,当前进度: step=' + state.step); // 保存当前首页地址(供跨页面跳转用) saveIndexUrl(); // 等待 M_PersonId 等变量就绪 waitFor(function () { return typeof M_PersonId !== 'undefined' && M_PersonId && M_PersonId.length > 0; }, 15000, function (ok) { if (!ok) { log('等待 M_PersonId 超时,可能未登录'); state.errors.push('index: M_PersonId timeout'); handleStepFailure(state); return; } accountId = normalizeAccountId(M_PersonId); state = getState(accountId); summary = getWorkflowSummary(state); if (!isRunActive(accountId) || !startLockHeartbeat(accountId)) { setRunActive(false, accountId); log('账号已切换或另一个标签页正在执行,本页已停止。'); return; } // 等待 M_Code 就绪(由 ChangeIndexJF AJAX 设置) waitFor(function () { return typeof M_Code !== 'undefined' && areIndexVarsReady(M_Code); }, 15000, function (varsReady) { if (!varsReady) { log('M_Code 未就绪,自动重试中...'); state.errors.push('index: required vars timeout'); handleStepFailure(state); return; } // 保存变量 saveVars(); log('首页变量已就绪'); // 再等一下让首页变量加载,然后以服务器积分明细校准五项任务进度。 setTimeout(function () { if (isPauseRequested(accountId) || !isRunActive(accountId)) return; saveVars(); reconcilePendingOnIndex(state, function (mayContinue) { if (!mayContinue || isPauseRequested(accountId) || !isRunActive(accountId)) return; getDailyScoreSnapshot(accountId, function (scoreSnapshot, scoreError) { if (isPauseRequested(accountId) || !isRunActive(accountId)) return; if (!scoreSnapshot) { state.errors.push('index: daily score sync failed'); log('读取服务器今日积分明细失败,自动重试中:' + (scoreError || '响应无效')); handleStepFailure(state); return; } applyDailyScoreSnapshot(state, scoreSnapshot); state.errors = clearErrorsByPrefixes(state.errors, ['index: daily score sync failed']); renderDailyTaskPlan(state); summary = getWorkflowSummary(state); state.serverDailyTotal = summary.total; state.step = summary.nextStep; saveState(state); log('服务器积分已同步:' + summary.total + '/' + getDailyTaskPlan(state).targetTotal + ' 分'); if (summary.completed) { showResults(state); state.step = 7; saveState(state); setRunActive(false, state.accountId); releaseExecutionLock(state.accountId); return; } switch (state.step) { case 0: // 登录签到 confirmSignInTask(state, function (ok) { if (!ok) { handleStepFailure(state); return; } state.step = getWorkflowSummary(state).nextStep; state.stepRetries = 0; saveState(state); setTimeout(function () { main(); }, 1000); }); break; case 1: // 读文章 doArticleTask(state, function (ok) { if (!ok) { handleStepFailure(state); return; } state.errors = clearErrorsByPrefixes(state.errors, ['article:']); state.step = getWorkflowSummary(state).nextStep; state.stepRetries = 0; saveState(state); setTimeout(function () { main(); }, 1000); }); break; case 2: // 进入练习 enterPractice(state); break; case 3: // 进入手机考试 enterExam(state, 'mobile'); break; case 4: // 进入模拟考试;未达到24分时返回首页后继续下一场 enterExam(state, 'simulate'); break; case 6: // 完成 showResults(state); state.step = 7; saveState(state); setRunActive(false, state.accountId); releaseExecutionLock(state.accountId); break; default: log('未知 step: ' + state.step); } }); }); }, 2000); }); }); } else if (isPractice) { // ===== 练习页 ===== log('练习页加载完成'); loadVars(state.accountId); if (state.step === 2) { doPracticeTask(state); } else { log('练习页: 当前 step=' + state.step + ',非预期,跳回首页'); setTimeout(function () { if (!isPauseRequested(state.accountId)) location.href = getIndexUrl(); }, 2000); } } else if (isExam) { // ===== 考试页 ===== log('考试页加载完成'); loadVars(state.accountId); if (state.step === 3 || state.step === 4) { doExamTask(state); } else { log('考试页: 当前 step=' + state.step + ',非预期,跳回首页'); setTimeout(function () { if (!isPauseRequested(state.accountId)) location.href = getIndexUrl(); }, 2000); } } else { // 其他页面,不处理 } } // Node 只导出正式纯函数,不启动浏览器流程。 if (typeof module !== 'undefined' && module.exports && typeof process !== 'undefined' && process.versions && process.versions.node) { module.exports = { allowedKeyHashes: ALLOWED_KEY_HASHES.slice(), sha256Fallback: sha256Fallback, sha256Hex: sha256Hex, normalizeKey: normalizeKey, isStoredLicenseValid: isStoredLicenseValid, parseApiSuccess: parseApiSuccess, answersMatch: answersMatch, getAnswerToggleIndexes: getAnswerToggleIndexes, buildExamAnswerPayload: buildExamAnswerPayload, fixAllShiTiAnswers: fixAllShiTiAnswers, parseExamScore: parseExamScore, parseExamPassStatus: parseExamPassStatus, calculateExamPoints: calculateExamPoints, evaluateExamScore: evaluateExamScore, evaluateExamSubmission: evaluateExamSubmission, evaluateScheduledExamSubmission: evaluateScheduledExamSubmission, canSubmitScheduledExamAsFullScore: canSubmitScheduledExamAsFullScore, getExamSubmissionPlan: getExamSubmissionPlan, canContinueAfterExamSave: canContinueAfterExamSave, calculatePracticePoints: calculatePracticePoints, parseDailyScoreSnapshot: parseDailyScoreSnapshot, describeDailyScoreSnapshotFailure: describeDailyScoreSnapshotFailure, applyDailyScoreSnapshot: applyDailyScoreSnapshot, remainingArticleDelay: remainingArticleDelay, isScoreLimitReached: isScoreLimitReached, normalizeVarsCache: normalizeVarsCache, normalizeRunControl: normalizeRunControl, normalizePauseControl: normalizePauseControl, transitionPauseControl: transitionPauseControl, canAcquireExecutionLock: canAcquireExecutionLock, getRequestFailureAction: getRequestFailureAction, reconcilePendingWrite: reconcilePendingWrite, sanitizeDebugText: sanitizeDebugText, parseFirstIndexTotal: parseFirstIndexTotal, getConfirmedSignInScore: getConfirmedSignInScore, Esdt: Esdt, getPracticeSessionAction: getPracticeSessionAction, getPracticeEntryAction: getPracticeEntryAction, getExamNativeEntryPlan: getExamNativeEntryPlan, getExamPaperIndex: getExamPaperIndex, findUnfinishedExamId: findUnfinishedExamId, findMatchingExamItem: findMatchingExamItem, selectScheduledExamCandidate: selectScheduledExamCandidate, getScoreLimitAction: getScoreLimitAction, applyScoreLimits: applyScoreLimits, clearErrorsByPrefixes: clearErrorsByPrefixes, getStepFailureAction: getStepFailureAction, getManualStartAction: getManualStartAction, markTaskSkipped: markTaskSkipped, isTaskSkipped: isTaskSkipped, getDailyTaskPlan: getDailyTaskPlan, getWorkflowSummary: getWorkflowSummary, asyncAjaxRequest: asyncAjaxRequest, resolveExamKsmxid: resolveExamKsmxid, isExamPageError: isExamPageError, getLocalDateKey: getLocalDateKey, normalizeState: normalizeState, areIndexVarsReady: areIndexVarsReady }; return; } // ========== 启动 ========== // 延迟启动,确保页面 jQuery 等库已加载 var startRetries = 0; function start() { if (typeof $ !== 'undefined' && typeof Esdt !== 'undefined') { requireLicense(main); } else if (++startRetries > 100) { // 等待约 30 秒仍未就绪,放弃并提示 console.error('[彬煤自动积分] 等待页面依赖库(jQuery)超时,脚本未启动'); showTip('页面依赖库未加载,脚本无法运行'); } else { setTimeout(start, 300); } } function initializeUserscript() { var accountId = getCurrentAccountId(); var monthlyExamState = getMonthlyExamState(accountId); accountId = accountId || monthlyExamState.accountId; var pauseControl = getPauseControl(accountId); if (pauseControl.status === 'pausing') { pauseControl = transitionPauseControl(pauseControl, 'write-finished', { activeWrites: 0, now: Date.now() }); savePauseControl(pauseControl); } if (pauseControl.status === 'paused') { bmPaused = true; releaseExecutionLock(accountId); renderPauseButton(accountId); showTip('任务保持暂停。点“继续”后会重新核对进度。'); return; } bmPaused = false; if (monthlyExamState.active) { if (!monthlyExamState.accountId || !startLockHeartbeat(monthlyExamState.accountId)) { finishMonthlyExamAction(monthlyExamState, 'error', '另一个标签页正在执行,月考未启动。'); return; } showTip('检测到月考操作,正在继续...'); start(); return; } if (isRunActive(accountId)) { if (!accountId || !startLockHeartbeat(accountId)) { setRunActive(false, accountId); showTip('另一个标签页正在执行,或当前账号尚未识别;本页未启动。'); return; } showTip('检测到已点击开始的任务,正在继续...'); start(); } else { showTip('脚本已就绪,请点击“开始”按钮运行。'); } } // 页面加载只创建面板;只有当天运行标记为 active 才续跑。 if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(initializeUserscript, 500); } else { window.addEventListener('DOMContentLoaded', function () { setTimeout(initializeUserscript, 500); }); } })();