// ==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);
});
}
})();