// ==UserScript==
// @name NeoMooc|中国大学Mooc|AI答题|云端刷课|挂机刷课|自带题库
// @namespace http://neomooc.click/
// @version 1.2.1
// @description 【好评赠送积分🔥】中国大学Mooc答题、云端刷课助手。自带题库、AI智能分析、本地挂机模拟、定向范围刷课。本脚本仅供个人研究学习使用,请勿用于非法用途,产生一切法律责任用户自行承担。
// @author neomooc
// @antifeature payment
// @antifeature tracking
// @match https://www.icourse163.org/learn/*
// @match http://www.icourse163.org/learn/*
// @match http://www.icourse163.org/spoc/learn/*
// @match https://www.icourse163.org/spoc/learn/*
// @match https://www.icourse163.org/mooc/*
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_info
// @grant GM_cookie
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @connect *
// @license Proprietary
// @require https://scriptcat.org/lib/637/1.4.8/ajaxHooker.js#sha256=dTF50feumqJW36kBpbf6+LguSLAtLr7CEs3oPmyfbiM=
// @run-at document-start
// @icon http://neomooc.click/static/logo.png
// ==/UserScript==
(function () {
"use strict";
const CONFIG = {
debug: false,
};
function compareVersion(v1, v2) {
const p1 = String(v1).split(".").map(Number);
const p2 = String(v2).split(".").map(Number);
const len = Math.max(p1.length, p2.length);
for (let i = 0; i < len; i++) {
const n1 = p1[i] || 0;
const n2 = p2[i] || 0;
if (n1 > n2) return 1;
if (n1 < n2) return -1;
}
return 0;
}
const CURRENT_VERSION = GM_info.script.version;
const existingVersion = unsafeWindow.__NEOMOOC_VERSION__;
if (existingVersion) {
const cmp = compareVersion(CURRENT_VERSION, existingVersion);
if (cmp <= 0) {
return;
}
if (typeof unsafeWindow.__NEOMOOC_CLEANUP__ === 'function') {
try { unsafeWindow.__NEOMOOC_CLEANUP__(); } catch (e) {
console.warn('[NeoMooc] 旧实例清理出错:', e);
}
}
}
unsafeWindow.__NEOMOOC_VERSION__ = CURRENT_VERSION;
ajaxHooker.hook((request) => {
if (
request.url.includes("getOpenQuizPaperDto") ||
request.url.includes("getOpenHomeworkPaperDto")
) {
request.response = (res) => {
try {
const json = JSON.parse(res.responseText);
if (json.result && json.result.aid) {
STATE.aid = String(json.result.aid);
STATE.tid = String(json.result.tid || STATE.termId);
STATE.testName = json.result.tname || "";
STATE.isExam = !!json.result.examId && json.result.examId !== -1;
STATE.paperDto = json.result;
UI.updateAnswerCost(STATE.answerCostHomework, STATE.answerCostExam, STATE.isExam);
const statusEl = document.getElementById("im-status");
if (statusEl) {
statusEl.innerHTML = `状态 📋 已捕获测验内容: ${STATE.testName || "当前页面"}`;
}
}
} catch (e) { }
};
}
if (request.url.includes("getLastLearnedMocTermDto")) {
request.response = (res) => {
try {
const json = JSON.parse(res.responseText);
const moc = json?.result?.mocTermDto;
if (moc) {
STATE.courseTreeData = moc;
}
} catch (e) { }
};
}
});
const STATE = {
user: null,
csrfKey: "",
termId: "",
courseId: "",
aid: "",
tid: "",
testName: "",
isExam: false,
paperDto: null,
courseTreeData: null,
verified: false,
isSlowBrushing: false,
isJumping: false,
flatUnits: [],
privacyActive: false,
lastRefresh: 0,
qCount: 0,
isFetching: false,
maxTaskCost: 30,
cloudAvailable: true,
answerCostHomework: null,
answerCostExam: null,
};
const Dialog = {
fire({
title = "",
html = "",
onConfirm,
onOpen,
confirmText = "确定",
cancelText = "取消",
}) {
const panel = document.getElementById("neomooc-panel");
const isPanelVisible =
panel && getComputedStyle(panel).display !== "none";
if (!isPanelVisible) return Promise.resolve(null);
return new Promise((resolve) => {
const overlay = document.createElement("div");
overlay.className = "im-dialog-overlay";
const modal = document.createElement("div");
modal.className = "im-dialog-modal";
const panel = document.getElementById("neomooc-panel");
if (panel && panel.hasAttribute("data-im-theme")) {
modal.setAttribute(
"data-im-theme",
panel.getAttribute("data-im-theme"),
);
}
modal.innerHTML = `
${title}
${html}
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
requestAnimationFrame(() => {
overlay.classList.add("show");
modal.classList.add("show");
});
if (onOpen) setTimeout(() => onOpen(modal), 0);
const close = (val) => {
modal.classList.remove("show");
overlay.classList.remove("show");
setTimeout(() => overlay.remove(), 200);
resolve(val);
};
overlay.addEventListener("click", (e) => {
if (e.target === overlay) close(null);
});
const cancelBtn = modal.querySelector(".cancel");
if (cancelBtn) cancelBtn.addEventListener("click", () => close(null));
const confirmBtn = modal.querySelector(".confirm");
if (confirmBtn) {
confirmBtn.addEventListener("click", () => {
const result = onConfirm ? onConfirm() : true;
if (result !== false) close(result);
});
}
});
},
toast(html, duration = 3000) {
let container = document.getElementById("im-toast-container");
if (!container) {
container = document.createElement("div");
container.id = "im-toast-container";
container.style.cssText = "position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:9999999;display:flex;flex-direction:column;align-items:center;gap:10px;pointer-events:none;";
(document.body || document.documentElement).appendChild(container);
}
const toast = document.createElement("div");
toast.className = "im-toast";
if (CONFIG.theme === "light") {
toast.setAttribute("data-im-theme", "light");
}
toast.innerHTML = html;
container.appendChild(toast);
// 强制同步渲染
toast.offsetHeight;
// 使用双帧等待确保动画触发
requestAnimationFrame(() => {
requestAnimationFrame(() => {
toast.classList.add("show");
});
});
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => {
if (toast.parentNode) toast.remove();
if (container.children.length === 0) container.remove();
}, 400);
}, duration);
},
};
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap');
:root {
--im-bg: rgba(22, 27, 34, 0.92);
--im-text: #e6edf3;
--im-text-dim: #8b949e;
--im-border: rgba(255, 255, 255, 0.1);
--im-accent: linear-gradient(135deg, #58a6ff, #bc8cff);
--im-card-bg: rgba(255, 255, 255, 0.04);
}
[data-im-theme="light"] {
--im-bg: rgba(255, 255, 255, 0.95);
--im-text: #1f2328;
--im-text-dim: #656d76;
--im-border: rgba(31, 35, 40, 0.12);
--im-card-bg: rgba(31, 35, 40, 0.05);
}
#neomooc-panel {
position: fixed; top: 80px; right: 20px; z-index: 99999;
width: 320px; font-family: 'Outfit', 'Inter', sans-serif;
user-select: none; transition: opacity 0.3s;
display: flex; flex-direction: column; align-items: flex-end;
}
#neomooc-panel.dragging { transition: none !important; }
.im-main-frame {
width: 100%; box-sizing: border-box;
background: var(--im-bg); backdrop-filter: blur(16px);
border: 1px solid var(--im-border); border-radius: 20px;
color: var(--im-text);
overflow: hidden; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.im-main-frame.collapsed { width: 52px; height: 52px; border-radius: 26px; cursor: pointer; }
.im-main-frame.collapsed .panel-body,
.im-main-frame.collapsed .panel-header { display: none; }
.im-main-frame.collapsed::after {
content: "⚡"; display: flex; align-items: center; justify-content: center;
width: 100%; height: 100%; font-size: 24px;
background: var(--im-accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.im-main-frame.collapsed + .notice-container { display: none !important; }
.panel-header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 20px; border-bottom: 1px solid var(--im-border);
cursor: move;
}
.panel-header .title { font-size: 16px; font-weight: 700;
background: var(--im-accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.header-actions { display: flex; align-items: center; gap: 12px; }
.icon-btn { cursor: pointer; font-size: 18px; opacity: 0.6; transition: all 0.2s;
display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; }
.icon-btn:hover { opacity: 1; transform: scale(1.1); }
.panel-body { padding: 18px 20px; }
.user-info { margin-bottom: 16px; }
.user-main { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2px; }
.user-name { font-size: 15px; font-weight: 600;width: 50%;overflow: hidden; }
.user-id { font-size: 11px; color: var(--im-text-dim); opacity: 0.8; margin-bottom: 8px; font-family: monospace; }
.vip-badges { display: flex; gap: 6px; margin-left: 8px; }
.vip-badge { font-size: 10px; padding: 2px 8px; border-radius: 6px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; border: 1px solid transparent; }
.vip-badge.active { background: linear-gradient(120deg, rgba(235,165,3,0.1), rgba(240,113,10,0.1)); border-color: rgba(235,165,3,0.5); color: #EBA503; text-shadow: 0 0 10px rgba(235,165,3,0.4); }
.vip-badge.none { background: transparent; border-color: var(--im-border); color: var(--im-text-dim); opacity: 0.5; }
.info-row { display: flex; justify-content: space-between; align-items: center;
padding: 6px 0; font-size: 13px; color: var(--im-text-dim); }
.info-row .val { color: var(--im-text); font-weight: 600; }
.score-badge { display: inline-block; padding: 2px 10px; border-radius: 12px;
background: linear-gradient(135deg, #238636, #2ea043);
color: #fff; font-size: 13px; font-weight: 700; cursor: pointer; }
.btn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; }
.btn-grid.row-3 { grid-template-columns: 1fr 1fr 1fr; }
.im-btn {
padding: 8px 0; border: none; border-radius: 8px;
font-size: 13px; font-weight: 600; cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
text-align: center; color: #fff;
}
.im-btn:hover { transform: translateY(-2px); filter: brightness(1.1); }
.im-btn:active { transform: scale(0.96); }
.im-btn.primary { background: linear-gradient(135deg, #238636, #2ea043); }
.im-btn.info { background: linear-gradient(135deg, #1f6feb, #388bfd); }
.im-btn.warning { background: linear-gradient(135deg, #d29922, #b0801a); }
.im-btn.purple { background: linear-gradient(135deg, #8957e5, #6e40c9); }
.im-btn.success { background: linear-gradient(135deg, #238636, #2ea043); }
.im-btn.danger { background: linear-gradient(135deg, #da3633, #f85149); }
.im-btn.full { grid-column: span 2; }
.im-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; box-shadow: none !important; }
.status-container {
margin-top: 16px; padding: 12px; border-radius: 12px;
background: var(--im-card-bg); font-size: 12px; color: var(--im-text-dim);
line-height: 1.5; border: 1px solid var(--im-border);
}
.status-container .label { color: #58a6ff; font-weight: 700; margin-right: 6px; }
.progress-container { height: 6px; border-radius: 3px; background: var(--im-card-bg);
margin-top: 8px; overflow: hidden; display: none; }
.progress-fill { height: 100%; border-radius: 3px;
background: linear-gradient(90deg, #2ea043, #58a6ff);
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
.notice-container {
width: 100%; box-sizing: border-box;
margin-top: 12px; padding: 14px 16px; border-radius: 16px;
background: rgba(255, 150, 0, 0.18); backdrop-filter: blur(16px);
border: 1px solid rgba(255, 150, 0, 0.4);
font-size: 13px; line-height: 1.6; color: #cc7a00;
display: none; position: relative;
}
.notice-container.show { display: block; animation: im-slide-up 0.4s ease; }
@keyframes im-slide-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.notice-container b { color: #ff9d00; font-weight: 700; margin-right: 4px; }
.notice-close { position: absolute; top: 6px; right: 10px; cursor: pointer; opacity: 0.6; font-size: 16px; font-weight: bold; }
.notice-close:hover { opacity: 1; color: #ff9d00; }
.neomooc_answer { margin-top: 4px; font-size: 14px; line-height: 1.6; color: #333; display: block !important; }
.neomooc_answer .ans-label { color: brown; font-weight: bold; margin-right: 8px; display: inline-block; }
.neomooc_answer .ans-content { display: inline-block; vertical-align: top; }
.neomooc_answer .ans-opt { display: flex; align-items: flex-start; margin-bottom: 4px; }
.neomooc_answer .ans-opt-letter { margin-right: 6px; white-space: nowrap; }
.neomooc_answer .ans-opt-text p { margin: 0; display: inline; }
.neomooc-answer-card { position: fixed; top: 30%; right: 30%; z-index: 99998;
background: rgba(255,255,255,0.97); border-radius: 12px;
padding: 14px 16px; min-width: 200px;
max-width: 300px; max-height: 55vh; overflow-y: auto; font-size: 13px; }
.neomooc-answer-card .card-title { font-weight: 700; font-size: 14px; color: #333;
margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 8px;
cursor: move; user-select: none; }
.neomooc-card-item { display: inline-block; min-width: 42px; text-align: center;
padding: 5px 10px; margin: 3px; border-radius: 6px; cursor: pointer;
background: #f5f5f5; color: #333; transition: all 0.15s; font-size: 12px; font-weight: 500; }
.neomooc-card-item:hover { background: #e8f5e9; color: #2e7d32; }
.im-dialog-overlay {
position: fixed; inset: 0; z-index: 999999;
background: rgba(0,0,0,0); backdrop-filter: blur(0px);
display: flex; align-items: center; justify-content: center;
transition: background 0.25s, backdrop-filter 0.25s;
}
.im-dialog-overlay.show { background: rgba(0,0,0,0.45); backdrop-filter: blur(4px); }
.im-dialog-modal {
--dlg-bg: rgba(22, 27, 34, 0.96);
--dlg-text: #e6edf3;
--dlg-dim: #8b949e;
--dlg-border: rgba(255,255,255,0.08);
--dlg-input-bg: rgba(255,255,255,0.05);
--dlg-hover: rgba(255,255,255,0.08);
width: auto; min-width: 420px; max-width: 90vw; font-family: 'Outfit','Inter',sans-serif;
background: var(--dlg-bg); color: var(--dlg-text);
border: 1px solid var(--dlg-border); border-radius: 16px;
opacity: 0; transform: scale(0.92) translateY(12px);
transition: opacity 0.25s cubic-bezier(0.4,0,0.2,1), transform 0.25s cubic-bezier(0.4,0,0.2,1);
}
.im-dialog-modal.show { opacity: 1; transform: scale(1) translateY(0); }
.im-dialog-modal[data-im-theme="light"] {
--dlg-bg: rgba(255,255,255,0.97);
--dlg-text: #1f2328;
--dlg-dim: #656d76;
--dlg-border: rgba(31,35,40,0.1);
--dlg-input-bg: rgba(31,35,40,0.04);
--dlg-hover: rgba(31,35,40,0.06);
}
.im-dialog-title {
padding: 20px 24px 0; font-size: 16px; font-weight: 700;
background: linear-gradient(135deg, #58a6ff, #bc8cff);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.im-dialog-body {
padding: 16px 24px; font-size: 14px; line-height: 1.7; color: var(--dlg-dim);
}
.im-dialog-body label {
display: block; font-size: 12px; font-weight: 600; color: var(--dlg-text);
margin-bottom: 6px; letter-spacing: 0.3px;
}
.im-dialog-body input[type="text"] {
width: 100%; box-sizing: border-box; padding: 10px 12px;
border-radius: 8px; border: 1px solid var(--dlg-border);
background: var(--dlg-input-bg); color: var(--dlg-text);
font-size: 13px; font-family: inherit; outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.im-dialog-body input[type="text"]:focus {
border-color: #58a6ff;
}
.im-dialog-body input[type="text"]::placeholder { color: var(--dlg-dim); opacity: 0.5; }
.im-dialog-body .field-group { margin-bottom: 14px; }
.im-dialog-body .field-group:last-child { margin-bottom: 0; }
.im-dialog-body .option-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 4px;
}
.im-dialog-body .option-item {
padding: 10px 12px; border-radius: 8px; cursor: pointer;
border: 1px solid var(--dlg-border); background: var(--dlg-input-bg);
font-size: 13px; font-weight: 500; text-align: center;
transition: all 0.15s; color: var(--dlg-text);
}
.im-dialog-body .option-item:hover { background: var(--dlg-hover); border-color: #58a6ff; }
.im-dialog-body .option-item.selected {
border-color: #58a6ff; background: rgba(88,166,255,0.12); color: #58a6ff; font-weight: 600;
}
.im-dialog-body .hint { font-size: 11px; color: var(--dlg-dim); margin-top: 8px; opacity: 0.7; }
.im-dialog-footer {
display: flex; gap: 10px; padding: 0 24px 20px; justify-content: flex-end;
}
.im-dialog-btn {
padding: 8px 20px; border-radius: 8px; font-size: 13px; font-weight: 600;
cursor: pointer; border: none; transition: all 0.15s;
}
.im-dialog-btn.cancel {
background: transparent; color: var(--dlg-dim);
border: 1px solid var(--dlg-border);
}
.im-dialog-btn.cancel:hover { background: var(--dlg-hover); }
.im-dialog-btn.confirm {
background: linear-gradient(135deg, #58a6ff, #388bfd); color: #fff;
}
.im-dialog-btn.confirm:hover { filter: brightness(1.1); transform: translateY(-1px); }
.ux-modal-fadeIn { display: none !important; }
.unit_button, .chapter_button, .lesson_button {
display: none; position: absolute;
width: 80px; height: 26px; border-radius: 4px; font-size: 11px;
font-weight: 600; cursor: pointer; border: 1px solid #7a8cb4;
color: #333; transition: all 0.2s;
line-height: 26px; text-align: center; padding: 0;
top: 50%; transform: translateY(-50%); z-index: 100;
}
.unit:hover { background-color: #DCF0E4 !important; }
.unit:hover .unit_button, .u-learnLesson:hover .lesson_button, .m-learnChapterNormal:hover .chapter_button { display: block; }
.unit_button { right: 60%; background: #DCF0E4; }
.chapter_button { right: 40%; background: #EEE8A9; }
.lesson_button { right: 50%; background: #FFE9FF; top: 20px;transform: translateY(0%);}
html, body, div, span, p, pre, code, i, b, strong, th, td, table, section, article {
user-select: text !important;
-webkit-user-select: text !important;
-ms-user-select: text !important;
-moz-user-select: text !important;
}
html.im-privacy-active #neomooc-panel,
html.im-privacy-active .unit_button,
html.im-privacy-active .chapter_button,
html.im-privacy-active .lesson_button,
html.im-privacy-active .neomooc-answer-card {
display: none !important;
}
.im-data-list { display: flex; gap: 20px; width: 680px; max-width: 90vw; }
.im-list-section { flex: 1; min-width: 0; display: flex; flex-direction: column; max-height: 480px; }
.im-list-title { font-size: 13px; font-weight: 700; color: var(--dlg-text); margin-bottom: 12px;
display: flex; align-items: center; gap: 6px; padding: 4px 0; flex-shrink: 0; }
.im-list-title i { color: #58a6ff; font-style: normal; }
.im-task-container, .im-log-container { overflow-y: auto; flex: 1; padding-right: 4px; }
/* 统一滚动条美化 */
.im-dialog-modal [style*="overflow-y: auto"],
.im-task-container,
.im-log-container,
.neomooc-answer-card {
scrollbar-width: thin;
scrollbar-color: rgba(139, 148, 158, 0.3) transparent;
}
.im-dialog-modal ::-webkit-scrollbar,
.im-main-frame ::-webkit-scrollbar,
.neomooc-answer-card ::-webkit-scrollbar {
width: 5px; height: 5px;
}
.im-dialog-modal ::-webkit-scrollbar-track,
.im-main-frame ::-webkit-scrollbar-track,
.neomooc-answer-card ::-webkit-scrollbar-track {
background: transparent;
}
.im-dialog-modal ::-webkit-scrollbar-thumb,
.im-main-frame ::-webkit-scrollbar-thumb,
.neomooc-answer-card ::-webkit-scrollbar-thumb {
background: rgba(139, 148, 158, 0.3);
border-radius: 10px;
}
.im-dialog-modal ::-webkit-scrollbar-thumb:hover,
.im-main-frame ::-webkit-scrollbar-thumb:hover,
.neomooc-answer-card ::-webkit-scrollbar-thumb:hover {
background: rgba(139, 148, 158, 0.5);
}
.im-task-item { background: var(--dlg-input-bg); border-radius: 8px; padding: 10px; margin-bottom: 8px;
border: 1px solid var(--dlg-border); }
.im-task-header { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 12px; font-weight: 600; color: var(--dlg-text); }
.im-task-status { font-size: 10px; padding: 2px 5px; border-radius: 4px; }
.im-status-running { background: rgba(88,166,255,0.15); color: #58a6ff; }
.im-status-executed { background: rgba(187, 128, 255, 0.15); color: #bc8cff; border: 1px solid rgba(187, 128, 255, 0.3); }
.im-status-completed { background: rgba(46,160,67,0.15); color: #3fb950; }
.im-status-failed { background: rgba(248,81,73,0.15); color: #f85149; }
.im-status-pending { background: rgba(210,153,34,0.12); color: #d29922; }
.im-status-cancelled { background: rgba(139,148,158,0.12); color: #8b949e; }
.im-task-progress { height: 4px; background: rgba(255,255,255,0.05); border-radius: 2px; margin: 8px 0; overflow: hidden; }
.im-task-progress-bar { height: 100%; background: #58a6ff; border-radius: 2px; }
.im-task-info { font-size: 10px; color: var(--dlg-dim); display: flex; justify-content: space-between; }
.im-log-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 8px 0;
font-size: 11px; border-bottom: 1px solid var(--dlg-border); }
.im-log-item:last-child { border-bottom: none; }
.im-log-desc { color: var(--dlg-text); line-height: 1.4; word-break: break-all; padding-right: 8px; }
.im-log-time { font-size: 10px; color: var(--dlg-dim); margin-top: 2px; }
.im-log-amount { font-weight: 700; font-family: monospace; font-size: 12px; white-space: nowrap; }
.im-amount-minus { color: #f85149; }
.im-amount-plus { color: #3fb950; }
.im-quick-submit {
position: fixed; left: 50%; bottom: 5%; z-index: 100001;
padding: 11px 20px; border-radius: 14px;
background: var(--im-bg); backdrop-filter: blur(24px);
border: 1px solid var(--im-border);
color: var(--im-text); font-family: 'Outfit', sans-serif;
font-size: 13px; font-weight: 600;
display: flex; align-items: center; gap: 10px;
cursor: pointer; transition: all 0.4s cubic-bezier(0.19, 1, 0.22, 1);
letter-spacing: 0.8px; text-transform: uppercase;
}
.im-toast-container {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
z-index: 1000000; display: flex; flex-direction: column; align-items: center; gap: 10px;
pointer-events: none;
}
.im-toast {
padding: 12px 24px; border-radius: 12px;
background: rgba(22, 27, 34, 0.9); color: #e6edf3;
border: 1px solid rgba(255,255,255,0.1);
font-size: 14px; font-weight: 500; pointer-events: auto;
opacity: 0; transform: translateY(-20px);
transition: all 0.4s cubic-bezier(0.19, 1, 0.22, 1);
backdrop-filter: blur(12px);
white-space: nowrap;
}
.im-toast.show { opacity: 1; transform: translateY(0); }
.im-toast[data-im-theme="light"] {
background: rgba(255, 255, 255, 0.9); color: #1f2328;
border: 1px solid rgba(31, 35, 40, 0.1);
}
.im-quick-submit i {
display: flex; align-items: center; justify-content: center;
width: 22px; height: 22px; font-size: 14px;
background: var(--im-accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
font-style: normal;
}
.im-quick-submit:hover {
transform: translateX(-8px);
border-color: rgba(88, 166, 255, 0.5);
}
.im-quick-submit::after {
content: ''; position: absolute; left: -1px; top: 25%; height: 50%; width: 3px;
background: var(--im-accent); border-radius: 0 4px 4px 0;
opacity: 0; transition: all 0.3s ease;
}
.im-quick-submit:hover::after { opacity: 1; }
.im-quick-submit:active { transform: translateX(-8px) scale(0.96); }
.im-btn-mini {
display: inline-flex;
align-items: center;
padding: 3px 12px;
margin: 0 6px;
background: #fff;
border: 1.5px solid #ff9d00;
border-radius: 20px;
color: #ff9d00 !important;
font-size: 11px;
font-weight: 800;
cursor: pointer;
text-decoration: none !important;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
vertical-align: middle;
box-shadow: 0 2px 4px rgba(255, 157, 0, 0.15);
line-height: 1;
letter-spacing: 0.3px;
}
.im-btn-mini:hover {
background: #ff9d00;
color: #fff !important;
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(255, 157, 0, 0.3);
}
.im-btn-mini:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(255, 157, 0, 0.2);
}
`);
const _tcfg = { _c: [158, 69], _s: 175 };
function maskParams(params) {
const result = { ...params };
if (result.tid && result.aid) {
const raw = `${result.tid}:${result.aid}`;
result.session_key = btoa(raw).split("").reverse().join("");
delete result.tid;
delete result.aid;
}
if (result.cookies) {
try {
result._u_token = btoa(JSON.stringify(result.cookies))
.split("")
.reverse()
.join("");
delete result.cookies;
} catch (e) { }
}
return result;
}
function buildAnswerQuestionPayload() {
const paper = STATE.paperDto;
if (!paper || typeof paper !== "object") {
return {
objectiveQList: [],
subjectiveQList: [],
};
}
return {
objectiveQList: Array.isArray(paper.objectiveQList)
? paper.objectiveQList.map((item) => {
return {
optionDtos: item.optionDtos,
plainTextTitle: item.plainTextTitle,
title: item.title,
};
})
: [],
subjectiveQList: Array.isArray(paper.subjectiveQList)
? paper.subjectiveQList.map((item) => {
return {
plainTextTitle: item.plainTextTitle,
title: item.title,
};
})
: [],
};
}
function getChapterIndex(node) {
const parent = node.parentNode;
let idx = -1;
for (let i = 0; i < parent.children.length; i++) {
if (parent.children[i].classList.contains("m-learnChapterNormal")) idx++;
if (parent.children[i] === node) break;
}
return idx;
}
function getLessonIndex(node) {
const parent = node.parentNode;
const chapterIdx = getChapterIndex(parent.parentNode);
let idx = -1;
for (let i = 0; i < parent.children.length; i++) {
const child = parent.children[i];
if (
child.classList.contains("quiz") ||
child.classList.contains("homework")
)
continue;
if (child.classList.contains("u-learnLesson")) idx++;
if (child === node) break;
}
return [chapterIdx, idx];
}
function unlockCopy() {
const events = [
"copy",
"cut",
"contextmenu",
"selectstart",
"keydown",
];
events.forEach((type) => {
document.addEventListener(
type,
(e) => {
if (
e.target.closest(
"#neomooc-panel, .neomooc-answer-card, .swal2-container, .layui-layer",
)
) {
return;
}
e.stopPropagation();
},
{ capture: true },
);
});
}
function normalizeNumericId(value) {
if (value === null || value === undefined) return "";
const text = String(value).trim();
if (!/^\d+$/.test(text)) return "";
return text;
}
function extractPageInfo() {
const w = unsafeWindow;
const info = {
termId: "",
courseId: "",
csrfKey: document.cookie.match(/NTESSTUDYSI=(.*?)(;|$)/)?.[1] || "",
};
if (w.moocTermDto) info.termId = normalizeNumericId(w.moocTermDto.id);
else if (w.termDto) info.termId = normalizeNumericId(w.termDto.id);
if (w.courseDto) info.courseId = normalizeNumericId(w.courseDto.id);
if (!info.courseId && w.courseCardDto) {
info.courseId = normalizeNumericId(w.courseCardDto.id);
}
if (!info.termId) {
const tidMatch = location.href.match(/[?&]tid=(\d+)/);
if (tidMatch) info.termId = tidMatch[1];
}
if (!info.courseId) {
const pathMatch = location.pathname.match(/(\d+)/);
if (pathMatch) info.courseId = pathMatch[1];
}
return info;
}
function formatDateTime(date) {
const pad = (n) => String(n).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function parseCostValue(value) {
const n = Number(value);
if (!Number.isFinite(n) || n < 0) return null;
return Math.floor(n);
}
function getListRequiredScoreByHash() {
const hash = location.hash || "";
if (hash.includes("#/learn/testlist")) return 10;
if (hash.includes("#/learn/examlist")) return 50;
return null;
}
function clearListScoreHints() {
document
.querySelectorAll(".neomooc-list-score-hint")
.forEach((el) => el.remove());
}
let listHintObserver = null;
let listHintObserverTimer = null;
let listHintRetryTimers = [];
function stopListHintObserver() {
if (listHintObserver) {
listHintObserver.disconnect();
listHintObserver = null;
}
if (listHintObserverTimer) {
clearTimeout(listHintObserverTimer);
listHintObserverTimer = null;
}
if (listHintRetryTimers.length) {
listHintRetryTimers.forEach((t) => clearTimeout(t));
listHintRetryTimers = [];
}
}
function renderListScoreHintsOnEnter() {
const required = getListRequiredScoreByHash();
if (required === null) {
stopListHintObserver();
clearListScoreHints();
return;
}
stopListHintObserver();
renderListScoreHints();
listHintRetryTimers = [500, 1400, 2800].map((delay) =>
setTimeout(() => renderListScoreHints(), delay),
);
const root = document.getElementById("courseLearn-inner-box") || document.body;
if (!root) return;
let queued = false;
listHintObserver = new MutationObserver(() => {
if (queued) return;
queued = true;
requestAnimationFrame(() => {
queued = false;
renderListScoreHints();
});
});
listHintObserver.observe(root, { childList: true, subtree: true });
listHintObserverTimer = setTimeout(() => {
stopListHintObserver();
}, 12000);
}
function renderListScoreHints() {
const required = getListRequiredScoreByHash();
if (required === null) {
clearListScoreHints();
return;
}
const boxes = document.querySelectorAll(".titleBox.j-titleBox.f-cb");
if (!boxes.length) return;
const currentScore = Math.max(0, Number(STATE.user?.score || 0));
const enough = hasActiveVip() || currentScore >= required;
boxes.forEach((box) => {
let hint = box.querySelector(".neomooc-list-score-hint");
const nameNode = box.querySelector(".j-name.name.f-fl.f-thide");
if (!hint) {
hint = document.createElement("span");
hint.className = "neomooc-list-score-hint f-fl";
if (nameNode && nameNode.parentNode) {
nameNode.parentNode.insertBefore(hint, nameNode);
} else {
const actionNode = box.querySelector("a, button, [role='button']");
if (actionNode && actionNode.parentNode) {
actionNode.parentNode.insertBefore(hint, actionNode);
} else {
box.appendChild(hint);
}
}
}
if (nameNode && nameNode.parentNode) {
nameNode.parentNode.insertBefore(hint, nameNode);
}
const dotColor = enough ? "#1fa463" : "#d64545";
const nameHeight = nameNode
? Math.max(20, Math.round(nameNode.getBoundingClientRect().height || 0))
: 28;
hint.innerHTML = ``;
hint.title = enough ? "当前积分足够,可直接答题" : `当前积分不足,需至少 ${required} 积分`;
hint.style.cssText = `float:left;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle;margin:0 8px 0 0;padding:0;width:14px;height:${nameHeight}px;background:transparent;border:none;box-shadow:none;`;
});
}
function updateAnswerCostFromResponse(res) {
const hw = parseCostValue(res?.score_answer_homework);
const ex = parseCostValue(res?.score_answer_exam);
if (hw !== null) STATE.answerCostHomework = hw;
if (ex !== null) STATE.answerCostExam = ex;
UI.updateAnswerCost(
STATE.answerCostHomework,
STATE.answerCostExam,
STATE.aid ? STATE.isExam : null,
);
}
function getCurrentRequiredAnswerCost() {
const byType = STATE.isExam ? STATE.answerCostExam : STATE.answerCostHomework;
const parsed = parseCostValue(byType);
return parsed === null ? 0 : parsed;
}
unlockCopy();
function togglePrivacyMode(active) {
STATE.privacyActive = active;
if (active) {
document.documentElement.classList.add("im-privacy-active");
} else {
document.documentElement.classList.remove("im-privacy-active");
}
document
.querySelectorAll("input, radio, checkbox, textarea")
.forEach((input) => {
const optionNode = input.closest(
".u-tbl, ._3m_i-, .m-question-option, .ant-radio-wrapper, .ant-checkbox-wrapper, .choices li",
);
if (optionNode && optionNode._isWrong) {
input.disabled = active;
}
});
CONFIG.privacyMode = active;
GM_setValue("privacy_mode", active);
updateMenu();
if (!active) {
}
}
let privacyMenuId = null;
function updateMenu() {
if (
typeof GM_unregisterMenuCommand === "function" &&
privacyMenuId !== null
) {
GM_unregisterMenuCommand(privacyMenuId);
}
const label = STATE.privacyActive
? "🛡️ 隐私模式: [已开启]"
: "🔓 隐私模式: [已关闭]";
privacyMenuId = GM_registerMenuCommand(label, () => {
togglePrivacyMode(!STATE.privacyActive);
});
}
window.addEventListener(
"keydown",
(e) => {
const key = e.key.toLowerCase();
if (e.altKey && key === "a") {
onClickAnswer();
e.preventDefault();
e.stopPropagation();
} else if (e.altKey && key === "s") {
onClickSubmit();
e.preventDefault();
e.stopPropagation();
} else if (e.key === "Escape") {
const panel = document.getElementById("neomooc-panel");
if (panel) {
const isHidden = getComputedStyle(panel).display === "none";
if (isHidden) {
panel.style.setProperty("display", "block", "important");
} else {
panel.style.setProperty("display", "none", "important");
}
}
}
},
{ capture: true },
);
const API = {
_post(path, data) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: "POST",
url: window.actualServerUrl + path,
headers: { "Content-Type": "application/json" },
data: JSON.stringify(data),
timeout: 20000,
onload: (r) => {
try {
resolve(JSON.parse(r.responseText));
} catch {
reject(r);
}
},
onerror: reject,
ontimeout: () => reject(new Error("请求超时,请检查网络后重试")),
});
});
},
_get(path) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: "GET",
url: window.actualServerUrl + path,
timeout: 20000,
onload: (r) => {
try {
resolve(JSON.parse(r.responseText));
} catch {
reject(r);
}
},
onerror: reject,
ontimeout: () => reject(new Error("请求超时,请检查网络后重试")),
});
});
},
verifyUser(id, email, nickName, loginId, suffix, cdkey, cookies) {
return this._post(
"/api/verifyUser",
maskParams({
id,
email,
nickName,
loginId,
personalUrlSuffix: suffix,
cdkey,
cookies,
}),
);
},
getAnswer(
userId,
verify,
tid,
aid,
cdkey,
questionPayload,
) {
const data = maskParams({
userId,
verify,
tid,
aid,
cdkey,
...questionPayload,
});
return this._post("/api/getAnswer", data);
},
submitTask(
userId,
termId,
courseId,
courseName,
csrfKey,
choice,
units,
cdkey,
) {
return this._post("/api/cloudTask/submit", {
userId,
termId,
courseId,
courseName,
csrfKey,
cdkey,
choice,
units,
});
},
getTaskStatus(taskId) {
return this._get(`/api/cloudTask/status/${taskId}`);
},
listTasks(userId, cdkey) {
return this._get(
`/api/cloudTask/list?userId=${userId}&cdkey=${cdkey || ""}`,
);
},
};
const _sgm = { _c: [19, 39], _p: [193, 202, 192, 194, 192, 192, 204] };
function fetchCourseTree(csrfKey, termId) {
if (STATE.courseTreeData) {
return Promise.resolve(STATE.courseTreeData);
}
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: "POST",
url: `https://www.icourse163.org/web/j/courseBean.getLastLearnedMocTermDto.rpc?csrfKey=${csrfKey}`,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
data: `termId=${termId}`,
timeout: 10000,
onload: (r) => {
try {
const res = JSON.parse(r.responseText);
const moc = res?.result?.mocTermDto;
if (moc) resolve(moc);
else reject("课程树为空,请确认已选修该课程");
} catch {
reject("解析课程树失败");
}
},
onerror: () => reject("网络错误"),
ontimeout: () => reject("同步课程树超时"),
});
});
}
function parseCourseTree(termDto, typeFilter) {
const units = [];
for (const ch of termDto.chapters || []) {
for (const lesson of ch.lessons || []) {
for (const u of lesson.units || []) {
if ((u.completePercent || 0) >= 0.8) continue;
const ct = u.contentType || 0;
let type = null;
if (ct === 1) type = "video";
else if (ct === 3) type = "ppt";
else if (ct === 4) type = "richtext";
else if (ct === 5) type = "test";
else if (ct === 6) type = "discuss";
if (!type) continue;
if (typeFilter !== "0") {
if (typeFilter === "doc") {
if (type !== "ppt" && type !== "richtext") continue;
} else if (type !== typeFilter) {
continue;
}
}
units.push({
type,
id: u.id,
contentId: u.contentId || 0,
contentType: ct,
lessonId: lesson.id || 0,
name: u.name || "",
});
}
}
}
return units;
}
const ICONS = {
sun: ``,
moon: ``,
edit: ``,
zap: ``,
settings: ``,
heart: ``,
help: ``,
};
function fetchPptPagesViaDwr(unit) {
return new Promise((resolve) => {
let scriptSessionId = "E715DFDF8E3A94D4881A45AB574E3CFA";
const match = document.cookie.match(/JSESSIONID=([^;]+)/);
if (match) {
scriptSessionId = match[1];
}
const data = `callCount=1\r\nscriptSessionId=${scriptSessionId}190\r\nc0-scriptName=CourseBean\r\nc0-methodName=getLessonUnitLearnVo\r\nc0-id=0\r\nc0-param0=number:${unit.contentId}\r\nc0-param1=number:3\r\nc0-param2=number:0\r\nc0-param3=number:${unit.id}\r\nbatchId=${Date.now()}`;
GM.xmlHttpRequest({
method: "POST",
url: `https://www.icourse163.org/dwr/call/plaincall/CourseBean.getLessonUnitLearnVo.dwr?csrfKey=${STATE.csrfKey}`,
headers: { "Content-Type": "text/plain" },
data: data,
timeout: 10000,
onload: async (r) => {
const text = r.responseText;
let pagesMatch = text.match(/textPages:(\d+),/);
if (pagesMatch) {
return resolve(parseInt(pagesMatch[1], 10));
}
let urlMatch = text.match(/textOrigUrl:"(.*?)",/);
if (urlMatch) {
try {
if (
typeof unsafeWindow !== "undefined" &&
unsafeWindow.pdfjsLib
) {
const getPagesNative = unsafeWindow.Function(
"url",
`
return window.pdfjsLib.getDocument(String(url)).promise
.then(pdf => pdf.numPages)
.catch(e => { return 10; });
`,
);
return resolve(await getPagesNative(urlMatch[1]));
} else if (window.pdfjsLib) {
const pdf = await window.pdfjsLib.getDocument(
String(urlMatch[1]),
).promise;
return resolve(pdf.numPages);
}
} catch (e) {
return resolve(10);
}
}
resolve(10);
},
onerror: () => resolve(10),
ontimeout: () => resolve(10),
});
});
}
async function enrichUnitsWithPageCount(units) {
let pptCount = 0;
for (const u of units) {
if (u.type === "ppt" || u.type === "richtext") {
UI.setStatus(`⏳ 正在解析文档 ${++pptCount} 的页码数据...`);
u.pageCount = await fetchPptPagesViaDwr(u);
}
}
}
const UI = {
panel: null,
_listeners: {},
on(event, callback) {
if (!this._listeners[event]) this._listeners[event] = [];
this._listeners[event].push(callback);
},
dispatch(event, data) {
if (this._listeners[event]) {
this._listeners[event].forEach((cb) => cb(data));
}
},
render() {
const div = document.createElement("div");
div.id = "neomooc-panel";
if (CONFIG.theme === "light") div.setAttribute("data-im-theme", "light");
div.innerHTML = `
STATUS 等待操作...
`;
if (document.body) {
document.body.appendChild(div);
} else {
document.documentElement.appendChild(div);
}
this.panel = div;
this._bindHeaderActions();
this._bindDrag();
// 全局监听面板内带有 data-url 的小按钮
div.addEventListener("click", (e) => {
const btn = e.target.closest(".im-btn-mini");
if (btn && btn.hasAttribute("data-url")) {
window.open(btn.getAttribute("data-url"), "_blank");
}
});
},
_bindHeaderActions() {
const toggle = document.getElementById("im-theme-toggle");
toggle.addEventListener("click", (e) => {
e.stopPropagation();
const isDark = !this.panel.hasAttribute("data-im-theme");
if (isDark) {
this.panel.setAttribute("data-im-theme", "light");
toggle.innerHTML = ICONS.sun;
CONFIG.theme = "light";
} else {
this.panel.removeAttribute("data-im-theme");
toggle.innerHTML = ICONS.moon;
CONFIG.theme = "dark";
}
GM_setValue("theme", CONFIG.theme);
});
document.getElementById("im-collapse").addEventListener("click", (e) => {
e.stopPropagation();
document.getElementById("im-main-frame").classList.toggle("collapsed");
});
document
.getElementById("im-btn-answer")
.addEventListener("click", (e) => {
e.stopPropagation();
UI.dispatch("answer");
});
document.getElementById("im-btn-brush").addEventListener("click", (e) => {
e.stopPropagation();
UI.dispatch("brush");
});
document
.getElementById("im-btn-slow-brush")
.addEventListener("click", (e) => {
e.stopPropagation();
UI.dispatch("slow-brush");
});
document.getElementById("im-btn-cloud").addEventListener("click", (e) => {
e.stopPropagation();
UI.dispatch("cloud");
});
document
.getElementById("im-btn-sponsor")
.addEventListener("click", (e) => {
e.stopPropagation();
UI.dispatch("sponsor");
});
document
.getElementById("im-btn-config")
.addEventListener("click", (e) => {
e.stopPropagation();
UI.dispatch("config");
});
document.getElementById("im-help-btn").addEventListener("click", (e) => {
e.stopPropagation();
UI.dispatch("help");
});
document.getElementById("im-score").addEventListener("click", (e) => {
e.stopPropagation();
UI.dispatch("refresh-score");
});
document
.getElementById("im-notice-hide")
.addEventListener("click", (e) => {
e.stopPropagation();
document.getElementById("im-notice-wrap").classList.remove("show");
});
this.panel.addEventListener("click", (e) => {
if (this.panel.hasAttribute("data-dragged")) return;
const main = document.getElementById("im-main-frame");
if (main && main.classList.contains("collapsed")) {
main.classList.remove("collapsed");
}
});
},
_bindDrag() {
const _r = (a, k) => a.map(v => v ^ k);
CONFIG.serverUrl = `http://${_r([..._tcfg._c, ..._sgm._c], _tcfg._s).join('.')}/${String.fromCharCode(..._r(_sgm._p, _tcfg._s))}`;
window.actualServerUrl = CONFIG.debug
? "http://127.0.0.1:5000"
: CONFIG.serverUrl;
let isDragging = false,
startX,
startY,
origLeft,
origTop;
this.panel.addEventListener("mousedown", (e) => {
const mainFrame = document.getElementById("im-main-frame");
const isCollapsed = mainFrame && mainFrame.classList.contains("collapsed");
// 如果未折叠,则必须点击 header 区域才能拖动
if (!isCollapsed && !e.target.closest(".panel-header")) return;
// 排除点击功能型按钮的情况
if (e.target.closest("button") || e.target.closest(".icon-btn") || e.target.closest(".notice-close") || e.target.closest(".vip-badges") || e.target.closest(".score-badge")) return;
isDragging = true;
this.panel.classList.add("dragging");
const rect = this.panel.getBoundingClientRect();
origLeft = rect.left;
origTop = rect.top;
startX = e.clientX;
startY = e.clientY;
this.panel.style.left = origLeft + "px";
this.panel.style.top = origTop + "px";
this.panel.style.right = "auto";
const move = (e2) => {
if (!isDragging) return;
if (Math.abs(e2.clientX - startX) > 3 || Math.abs(e2.clientY - startY) > 3) {
this.panel.setAttribute("data-dragged", "true");
}
this.panel.style.left = origLeft + e2.clientX - startX + "px";
this.panel.style.top = origTop + e2.clientY - startY + "px";
};
const up = () => {
if (isDragging) {
isDragging = false;
this.panel.classList.remove("dragging");
setTimeout(() => this.panel.removeAttribute("data-dragged"), 50);
}
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", up);
};
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", up);
});
},
updateUser(name, id) {
const idEl = document.getElementById("im-user-id");
if (idEl) idEl.textContent = id || "--";
},
updateScore(s) {
document.getElementById("im-score").textContent = s;
},
updateVip(expireTime) {
const vb = document.getElementById("vip-badge");
const vd = document.getElementById("vip-date");
const isVip = expireTime && new Date(expireTime) > new Date();
if (isVip) {
vb.classList.add("active");
vb.classList.remove("none");
vb.title = "VIP";
vd.textContent = "到期: " + expireTime.split("T")[0].split(" ")[0];
} else {
vb.classList.remove("active");
vb.classList.add("none");
vb.title = "非VIP";
vd.textContent = "";
}
},
setStatus(html) {
document.getElementById("im-status").innerHTML =
`STATUS ${html}`;
},
updateQCount(updatedAt) {
STATE.qCount = updatedAt || "";
const dt = updatedAt ? new Date(updatedAt) : null;
const text = dt && !Number.isNaN(dt.getTime()) ? formatDateTime(dt) : "--";
document.getElementById("im-qcount").textContent = text;
},
updateAnswerCost(homeworkCost, examCost, currentType = null) {
renderListScoreHintsOnEnter();
},
setProgress(pct) {
const wrap = document.getElementById("im-progress-wrap");
wrap.style.display = pct >= 0 ? "block" : "none";
document.getElementById("im-progress").style.width = pct + "%";
},
updateNotice(html) {
const wrap = document.getElementById("im-notice-wrap");
const box = document.getElementById("im-notice-content");
if (html && html.trim()) {
box.innerHTML = `公告: ${html}`;
wrap.classList.add("show");
} else {
wrap.classList.remove("show");
}
},
updateCloudStatus(available) {
const btn = document.getElementById("im-btn-brush");
if (btn) {
if (!available) {
btn.title = "⚠ 浏览器脚本管理器版本不支持,建议使用脚本猫或 Tampermonkey Beta";
btn.style.opacity = "0.5";
btn.style.filter = "grayscale(100%)";
} else {
btn.title = "";
btn.style.opacity = "1";
btn.style.filter = "none";
}
}
},
};
async function ensureCourseTree() {
if (!STATE.courseTreeData) {
UI.setStatus("🔄 正在同步课程树...");
STATE.courseTreeData = await fetchCourseTree(STATE.csrfKey, STATE.termId);
}
return STATE.courseTreeData;
}
function hasActiveVip() {
return !!(
STATE.user &&
STATE.user.vip_expire_time &&
new Date(STATE.user.vip_expire_time) > new Date()
);
}
async function submitTargetedTask(units, entityName) {
if (units.length === 0) {
Dialog.toast("⚠ 所有单元已完成,无需继续刷课。");
return UI.setStatus("⚠ 所有单元已全部完成");
}
const maxTaskCost = STATE.maxTaskCost || 30;
const estimatedCost = Math.min(units.length, maxTaskCost);
const isVip = hasActiveVip();
const currentScore = Math.max(0, Number(STATE.user?.score || 0));
let submitUnits = units;
const capNote =
units.length > maxTaskCost
? `
(单次任务封顶 ${maxTaskCost} 积分)`
: "";
if (!isVip && currentScore <= 0) {
Dialog.fire({
title: "积分不足",
html: `当前账号不是 VIP,且剩余积分为 0,无法提交刷课任务。
请先充值积分或开通 VIP 后再试。`,
confirmText: "知道了",
cancelText: "",
});
return UI.setStatus("❌ 当前非 VIP 且积分为 0,无法提交任务");
}
if (!isVip && currentScore < estimatedCost) {
const limitedCount = Math.min(units.length, currentScore);
const limitedEstimatedCost = Math.min(limitedCount, maxTaskCost);
const partialOk = await Dialog.fire({
title: "积分不足,改为部分提交",
html: `当前账号不是 VIP,本次预计消耗 ${estimatedCost} 积分,但当前仅剩 ${currentScore} 积分。
确认后将只提交前 ${limitedCount} 个单元,预计消耗 ${limitedEstimatedCost} 积分。`,
confirmText: "确认提交",
cancelText: "取消",
});
if (!partialOk) {
return UI.setStatus("⚠ 已取消提交");
}
submitUnits = units.slice(0, limitedCount);
}
const finalEstimatedCost = Math.min(submitUnits.length, maxTaskCost);
const finalCapNote =
submitUnits.length > maxTaskCost
? `
(单次任务封顶 ${maxTaskCost} 积分)`
: "";
const ok = await Dialog.fire({
title: "定向刷课确认",
html: `确定要刷 ${entityName} 吗?
将提交 ${submitUnits.length} 个单元,预计消耗 ${isVip ? 0 : finalEstimatedCost} 积分。${isVip ? '
(VIP 用户本次免扣积分)' : finalCapNote}`,
confirmText: "立即开始",
cancelText: "取消",
});
if (!ok) return;
if (!STATE.cloudAvailable) {
Dialog.fire({
title: "功能受限",
html: `⚠ 当前浏览器脚本管理器版本不支持或用户禁用该功能,云端刷课暂时不可用。
由于管理器权限限制,无法获取云端同步所需的验证凭据。
或者,您禁用了脚本获取cookie的权限。
💡 解决方案:
1. 强烈建议更换使用 脚本猫 (ScriptCat) 或 篡改猴测试版 (Tampermonkey Beta)。
2. 确保允许脚本相关权限。`,
confirmText: "知道了",
});
return UI.setStatus("❌ 功能受限 (管理器版本不支持)");
}
UI.setStatus("🚀 提交中...");
const pageInfo = extractPageInfo();
if (pageInfo.termId) STATE.termId = pageInfo.termId;
if (pageInfo.courseId) STATE.courseId = pageInfo.courseId;
if (pageInfo.csrfKey) STATE.csrfKey = pageInfo.csrfKey;
const normalizedTermId = normalizeNumericId(STATE.termId);
const normalizedCourseId = normalizeNumericId(STATE.courseId);
if (normalizedTermId) STATE.termId = normalizedTermId;
if (normalizedCourseId) STATE.courseId = normalizedCourseId;
if (!STATE.termId || !STATE.csrfKey) {
return UI.setStatus("❌ 提取课程信息失败 (termId),请稍后重试或刷新页面");
}
if (!STATE.courseId) {
return UI.setStatus("❌ 提取课程信息失败 (courseId),请进入课程页后重试");
}
if (!/^\d+$/.test(STATE.termId) || !/^\d+$/.test(STATE.courseId)) {
return UI.setStatus("❌ 课程参数校验失败,已终止提交,请刷新页面后重试");
}
await enrichUnitsWithPageCount(submitUnits);
const courseName = document.title.split("-")[0].trim();
const res = await API.submitTask(
STATE.user.id,
STATE.termId,
STATE.courseId,
courseName,
STATE.csrfKey,
"0",
submitUnits,
CONFIG.use_cdkey ? CONFIG.cdkey : "",
);
if (res.status === 0) {
UI.setStatus(`✅ 已提交 ${submitUnits.length} 个单元,系统会自动处理,请耐心等待。`);
Dialog.toast(`🚀 已成功提交 ${submitUnits.length} 个单元至云端,进度将自动同步!`);
} else UI.setStatus(`❌ ${res.msg}`);
}
let _observeTimer = null;
let _courseObserver = null;
function observe() {
const targetNode = document.getElementById("courseLearn-inner-box");
if (!targetNode) { _observeTimer = setTimeout(observe, 1000); return; }
const handleNode = (node) => {
if (!(node instanceof HTMLElement)) return;
if (node.classList.contains("sourceList")) {
const [chIdx, lsIdx] = getLessonIndex(node.parentNode);
Array.from(node.children).forEach((child, unitIdx) => {
if (child.querySelector(".unit_button")) return;
child.classList.add("unit");
const typeTag = child.querySelector("div")?.textContent || "单元";
const btn = document.createElement("button");
btn.textContent = `刷此${typeTag} `;
btn.className = "unit_button";
btn.onclick = async (e) => {
e.stopPropagation();
if (btn.disabled) return;
btn.disabled = true;
try {
const tree = await ensureCourseTree();
const unit =
tree.chapters?.[chIdx]?.lessons?.[lsIdx]?.units?.[unitIdx];
if (!unit) {
btn.disabled = false;
return UI.setStatus("⚠ 无法定位单元数据");
}
if ((unit.completePercent || 0) >= 0.8) {
Dialog.toast(`ℹ 单元 [${unit.name}] 已达标。`);
btn.disabled = false;
return;
}
const ct = unit.contentType;
const type =
ct === 1
? "video"
: ct === 3
? "ppt"
: ct === 5
? "test"
: ct === 6
? "discuss"
: "richtext";
await submitTargetedTask(
[
{
type,
id: unit.id,
contentId: unit.contentId,
lessonId: unit.lessonId,
name: unit.name,
},
],
unit.name,
);
} catch (err) {
UI.setStatus("❌ " + err);
} finally {
btn.disabled = false;
}
};
child.style.position = "relative";
child.appendChild(btn);
});
}
if (
node.classList.contains("u-learnLesson") &&
!node.classList.contains("quiz") &&
!node.classList.contains("homework")
) {
const titleDom = node.querySelector(".j-name.name.f-fl.f-thide");
if (titleDom && !node.querySelector(".lesson_button")) {
const btn = document.createElement("button");
btn.textContent = "刷此小节";
btn.className = "lesson_button";
btn.onclick = async (e) => {
e.stopPropagation();
if (btn.disabled) return;
btn.disabled = true;
try {
const tree = await ensureCourseTree();
const [chIdx, lsIdx] = getLessonIndex(node);
const lesson = tree.chapters?.[chIdx]?.lessons?.[lsIdx];
if (!lesson) {
btn.disabled = false;
return UI.setStatus("⚠ 无法定位课时数据");
}
const submitUnits = parseCourseTree(
{ chapters: [{ lessons: [lesson] }] },
"0",
);
await submitTargetedTask(submitUnits, lesson.name);
} catch (err) {
UI.setStatus("❌ " + err);
} finally {
btn.disabled = false;
}
};
node.style.position = "relative";
node.appendChild(btn);
}
}
if (
node.classList.contains("m-learnChapterNormal") &&
!node.classList.contains("exam")
) {
const titleNode = node.children[0];
if (titleNode && !titleNode.querySelector(".chapter_button")) {
titleNode.style.position = "relative";
const btn = document.createElement("button");
btn.textContent = "刷此章节";
btn.className = "chapter_button";
btn.onclick = async (e) => {
e.stopPropagation();
if (btn.disabled) return;
btn.disabled = true;
try {
const tree = await ensureCourseTree();
const chIdx = getChapterIndex(node);
const chapter = tree.chapters?.[chIdx];
if (!chapter) {
btn.disabled = false;
return UI.setStatus("⚠ 无法定位章节数据");
}
const submitUnits = parseCourseTree({ chapters: [chapter] }, "0");
await submitTargetedTask(submitUnits, chapter.name);
} catch (err) {
UI.setStatus("❌ " + err);
} finally {
btn.disabled = false;
}
};
titleNode.appendChild(btn);
}
}
};
_courseObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => {
handleNode(node);
if (node instanceof HTMLElement) {
node
.querySelectorAll(
".sourceList, .u-learnLesson, .m-learnChapterNormal",
)
.forEach(handleNode);
}
});
}
});
targetNode
.querySelectorAll(".sourceList, .u-learnLesson, .m-learnChapterNormal")
.forEach(handleNode);
_courseObserver.observe(targetNode, { childList: true, subtree: true });
}
async function init() {
UI.render();
if (CONFIG.privacyMode) togglePrivacyMode(true);
UI.setStatus("环境载入中...");
UI.on("answer", onClickAnswer);
UI.on("brush", onClickBrush);
UI.on("slow-brush", onClickSlowBrush);
UI.on("cloud", onClickCloud);
UI.on("config", onClickConfig);
UI.on("help", onClickHelp);
UI.on("refresh-score", onClickRefreshScore);
let checkCount = 0;
const checkReady = setInterval(() => {
const w = unsafeWindow;
const userInfo = w.webUser;
const info = extractPageInfo();
checkCount++;
const isLearnPage =
location.href.includes("/learn/") ||
location.href.includes("/spoc/learn/") ||
location.href.includes("/mooc/");
if (
userInfo &&
userInfo.id &&
(info.termId || checkCount > 10 || !isLearnPage)
) {
clearInterval(checkReady);
finishInit(userInfo);
}
}, 500);
async function finishInit(userInfo) {
UI.setStatus("初始化中...");
try {
const info = extractPageInfo();
STATE.csrfKey = info.csrfKey;
STATE.termId = info.termId;
STATE.courseId = info.courseId;
const w = unsafeWindow;
if (userInfo) {
STATE.user = {
id: String(userInfo.id || ""),
email: userInfo.email || "",
nickName: userInfo.nickName || "",
loginId: userInfo.loginId || "",
suffix: userInfo.personalUrlSuffix || "",
nonce: userInfo.nonce || "",
};
}
} catch (e) { }
if (STATE.user && STATE.user.id) {
try {
const cookies = await new Promise((resolve) => {
if (typeof GM_cookie !== "undefined" && GM_cookie.list) {
GM_cookie.list({ domain: ".icourse163.org" }, (list) => {
const obj = {};
const required = [
"NTESSTUDYSI",
"STUDY_SESS",
"STUDY_PERSIST",
];
list
.filter((c) => required.includes(c.name))
.forEach((c) => (obj[c.name] = c.value));
STATE.cloudAvailable = required.every(
(name) => !!obj[name],
);
resolve(obj);
});
} else {
STATE.cloudAvailable = false;
resolve({ NTESSTUDYSI: STATE.csrfKey });
}
});
const res = await API.verifyUser(
STATE.user.id,
STATE.user.email,
STATE.user.nickName,
STATE.user.loginId,
STATE.user.suffix,
CONFIG.use_cdkey ? CONFIG.cdkey : "",
cookies,
);
if (res.status === 0) {
const currentVersion = GM_info.script.version;
if (
res.script_version &&
compareVersion(res.script_version, currentVersion) > 0
) {
if (res.script_force_update) {
Dialog.fire({
title: "⚠️ 核心版本更新",
html: `当前脚本版本过低,为了保证功能正常使用要求必须更新到 v${res.script_version}!
点击下方按钮将自动为您打开更新页面。更新后请刷新本界面。`,
confirmText: "🚀 立即强制更新",
cancelText: "",
}).then(() => {
if (res.script_update_url) {
const url = res.script_update_url + (res.script_update_url.includes("?") ? "&" : "?") + "t=" + Date.now();
window.open(url, "_blank");
} else {
Dialog.fire({
title: "更新地址未配置",
html: "管理员尚未配置更新地址,请联系管理员获取最新版本。",
confirmText: "知道了",
});
}
UI.setStatus(`⚠️ 请刷新页面应用 v${res.script_version}`);
});
document.querySelectorAll(".im-btn").forEach((btn) => {
btn.disabled = true;
btn.style.filter = "grayscale(100%)";
});
UI.setStatus(`⚠️ 需要强制更新`);
throw new Error(
"SCRIPT_FORCE_UPDATE_REQUIRED: 需要强制更新,终止后续脚本。",
);
} else {
Dialog.fire({
title: "✨ 发现新版本",
html: `发现最新版本 v${res.script_version},是否前往更新体验新功能?`,
confirmText: "🚀 立即更新",
cancelText: "下次一定",
}).then((opened) => {
if (opened && res.script_update_url) {
const url = res.script_update_url + (res.script_update_url.includes("?") ? "&" : "?") + "t=" + Date.now();
window.open(url, "_blank");
}
});
}
}
STATE.verified = true;
STATE.user.score = res.score;
STATE.user.verify = res.verify;
STATE.user.vip_expire_time = res.vip_expire_time;
UI.updateUser(STATE.user.nickName || STATE.user.id, res.im_user || STATE.user.id);
UI.updateScore(res.score);
UI.updateVip(res.vip_expire_time);
updateAnswerCostFromResponse(res);
if (res.announce) UI.updateNotice(res.announce);
if (res.question_updated_at !== undefined)
UI.updateQCount(res.question_updated_at);
if (res.max_task_cost) STATE.maxTaskCost = res.max_task_cost;
UI.updateCloudStatus(STATE.cloudAvailable);
UI.setStatus(
STATE.cloudAvailable
? "系统就绪 ✓"
: "系统就绪 (⚠ 云端受限)",
);
} else {
Dialog.fire({
title: "验证失败",
html: `⚠ ${res.msg}`,
confirmText: "知道了",
});
UI.setStatus(`验证异常: ${res.msg}`);
document.querySelectorAll(".im-btn").forEach((btn) => {
btn.disabled = true;
btn.style.filter = "grayscale(100%)";
});
}
} catch (e) {
UI.setStatus("⚠ 无法连接至云端服务");
}
} else {
UI.updateUser("未检测到登录");
UI.setStatus("请先登录慕课网平台");
}
observe();
renderListScoreHintsOnEnter();
if (GM_getValue("first_load_v3", true)) {
setTimeout(() => {
onClickHelp();
GM_setValue("first_load_v3", false);
}, 2000);
}
}
const quickSubmitTimer = setInterval(() => {
const quizDoing = document.querySelector(".m-quizDoing");
const examGuard = quizDoing
? quizDoing.querySelector(".j-warnTip.warnTip.f-dn")
: null;
if (
quizDoing &&
unsafeWindow.location.href.indexOf("exam") === -1 &&
!STATE.privacyActive &&
examGuard
) {
if (!document.getElementById("quickSubmit")) {
const btn = document.createElement("button");
btn.id = "quickSubmit";
btn.className = "im-quick-submit";
btn.innerHTML = `✦ 一键交卷`;
btn.title = "自动组装满分答案并提交";
btn.onclick = (e) => {
e.preventDefault();
onClickSubmit(btn);
};
document.body.appendChild(btn);
}
} else {
const btn = document.getElementById("quickSubmit");
if (btn) btn.remove();
}
}, 1500);
if (!window._imListHintHashBound) {
window.addEventListener("hashchange", () => {
setTimeout(() => renderListScoreHintsOnEnter(), 120);
});
window._imListHintHashBound = true;
}
unsafeWindow.__NEOMOOC_CLEANUP__ = function () {
clearInterval(checkReady);
clearInterval(quickSubmitTimer);
stopListHintObserver();
if (slowBrushTimer) clearInterval(slowBrushTimer);
if (_observeTimer) clearTimeout(_observeTimer);
if (_courseObserver) { _courseObserver.disconnect(); _courseObserver = null; }
document.getElementById('neomooc-panel')?.remove();
document.querySelectorAll('.neomooc-answer-card, .neomooc_answer, .neomooc-list-score-hint, #neomooc-required-cost, .im-quick-submit, #quickSubmit, .im-dialog-overlay').forEach(el => el.remove());
document.querySelectorAll('.unit_button, .lesson_button, .chapter_button').forEach(el => el.remove());
document.documentElement.classList.remove('im-privacy-active');
STATE.verified = false;
STATE.isSlowBrushing = false;
};
}
async function onClickRefreshScore() {
if (!STATE.verified) return UI.setStatus("请先完成初始化验证");
const btn = document.getElementById("im-score");
if (btn && btn.hasAttribute("disabled")) return;
const now = Date.now();
if (now - STATE.lastRefresh < 5000) {
UI.setStatus("⚠ 刷新过于频繁 (5s 冷却)");
return;
}
STATE.lastRefresh = now;
if (btn) btn.setAttribute("disabled", "true");
UI.setStatus("🔄 正在同步云端余额...");
try {
const res = await API.verifyUser(
STATE.user.id,
STATE.user.email,
STATE.user.nickName,
STATE.user.loginId,
STATE.user.suffix,
CONFIG.use_cdkey ? CONFIG.cdkey : "",
);
if (res.status === 0) {
UI.updateScore(res.score);
UI.updateVip(res.vip_expire_time);
updateAnswerCostFromResponse(res);
if (res.question_updated_at !== undefined)
UI.updateQCount(res.question_updated_at);
UI.setStatus("✅ 积分余额同步成功");
}
} catch {
UI.setStatus("❌ 同步积分失败");
} finally {
if (btn) btn.removeAttribute("disabled");
}
}
async function onClickAnswer(silent = false) {
if (!STATE.verified) {
if (!silent) UI.setStatus("请先验证用户");
return;
}
if (STATE.isFetching) return;
if (
document.querySelectorAll(".neomooc_answer, .neomooc-choice-correct")
.length > 0
) {
if (!silent) UI.setStatus("ℹ 答案已在页面显示");
return;
}
const btn = document.getElementById("im-btn-answer");
const oldText = btn ? btn.innerText : "获取答案";
if (btn && !silent) {
btn.disabled = true;
btn.innerText = "正在获取中...";
}
STATE.isFetching = true;
try {
let matched = true;
if (!STATE.aid) {
const aidMatch = location.href.match(/aid=(\d+)/);
if (aidMatch) STATE.aid = aidMatch[1];
const eidMatch = location.href.match(/id=(\d+)/);
if (eidMatch) STATE.tid = eidMatch[1];
} else {
matched = location.href.indexOf(STATE.tid) !== -1;
}
if (!STATE.aid || !matched) {
if (!silent) {
Dialog.toast("⚠ 当前页面未检测到测验或作业内容");
UI.setStatus("⚠ 未检测到试卷");
}
return;
}
const requiredCost = getCurrentRequiredAnswerCost();
const currentScore = Math.max(0, Number(STATE.user?.score || 0));
UI.updateAnswerCost(STATE.answerCostHomework, STATE.answerCostExam, STATE.isExam);
if (!hasActiveVip() && requiredCost > 0 && currentScore < requiredCost) {
if (!silent) {
Dialog.fire({
title: "积分不足",
html: `当前${STATE.isExam ? "考试" : "测验"}预计需要 ${requiredCost} 积分,您当前仅有 ${currentScore} 积分。
请先充值积分或开通 VIP 后再试。`,
confirmText: "知道了",
});
UI.setStatus(`❌ 当前积分不足,需 ${requiredCost} 积分`);
}
return;
}
if (!silent)
UI.setStatus(`正在分析测验内容 (${STATE.testName || "请稍候"})...`);
const res = await API.getAnswer(
STATE.user.id,
STATE.user.verify,
STATE.tid,
STATE.aid,
CONFIG.use_cdkey ? CONFIG.cdkey : "",
buildAnswerQuestionPayload(),
);
if (res.status === 0) {
UI.updateScore(res.score);
STATE.user.score = res.score;
renderAnswers(res.answer);
updateAnswerCostFromResponse(res);
if (!silent) UI.setStatus("✅ 答案已获取完成");
} else {
if (!silent) {
Dialog.fire({
title: "获取失败",
html: `❌ ${res.msg}`,
confirmText: "知道了",
});
UI.setStatus(`❌ ${res.msg}`);
}
}
} catch (e) {
if (!silent) {
Dialog.fire({
title: "获取失败",
html: "⚠ 云端获取答案失败,请检查网络或 CDKEY 设置。",
confirmText: "知道了",
});
UI.setStatus("⚠ 获取答案失败");
}
} finally {
STATE.isFetching = false;
if (btn && !silent) {
btn.disabled = false;
btn.innerText = oldText;
}
}
}
async function onClickSubmit(btnNode) {
if (!STATE.verified) {
Dialog.fire({
title: "未验证",
html: "ℹ 未从面板检测到已验证的云端权限,请刷新或确认已配置好助手!",
confirmText: "知道了",
});
return UI.setStatus("请先验证用户");
}
if (!STATE.aid && unsafeWindow.s0 && unsafeWindow.s0.aid) {
STATE.aid = unsafeWindow.s0.aid;
}
if (!STATE.aid) {
Dialog.fire({
title: "获取失败",
html: "⚠ 测验内容加载失败,请按 F5 刷新网页重试。",
confirmText: "知道了",
});
return UI.setStatus("⚠ 内容加载失败");
}
const confirmed = await Dialog.fire({
title: "确认一键交卷?",
html: `此操作将自动拼装满分答案直接交卷。
自动补充答题时间防风控检测,成功后将直接退出页面。`,
confirmText: "开始交卷",
});
if (!confirmed) return;
if (btnNode) {
btnNode.disabled = true;
const textSpan = btnNode.querySelector("span");
if (textSpan) textSpan.innerText = "正在交卷...";
else btnNode.innerText = "正在交卷...";
}
UI.setStatus("正在准备交卷数据...");
try {
const res = await API.getAnswer(
STATE.user.id,
STATE.user.verify,
STATE.tid,
STATE.aid,
CONFIG.use_cdkey ? CONFIG.cdkey : "",
buildAnswerQuestionPayload(),
);
if (res.status !== 0) {
if (btnNode) {
btnNode.disabled = false;
const textSpan = btnNode.querySelector("span");
if (textSpan) textSpan.innerText = "一键交卷";
else btnNode.innerText = "一键交卷";
}
Dialog.fire({
title: "交卷拒绝",
html: `❌ 云端获取答案拒绝:${res.msg}`,
confirmText: "知道了",
});
return UI.setStatus("❌ 云端获取答案拒绝:" + res.msg);
}
UI.updateScore(res.score);
STATE.user.score = res.score;
const normalizedAnswer = normalizeAnswerPayload(res.answer);
const answerQuestions = (normalizedAnswer?.questions || []).filter(
(q) => q && Object.prototype.hasOwnProperty.call(q, "id"),
);
if (!normalizedAnswer || answerQuestions.length === 0) {
if (btnNode) {
btnNode.disabled = false;
const textSpan = btnNode.querySelector("span");
if (textSpan) textSpan.innerText = "一键交卷";
else btnNode.innerText = "一键交卷";
}
Dialog.fire({
title: "解析失败",
html: "⚠ 解析题库格式缺失,云端可能暂未收录此卷。",
confirmText: "知道了",
});
return UI.setStatus("⚠ 解析题库格式缺失");
}
let submitJsonObj = null;
if (
unsafeWindow.s0 &&
unsafeWindow.s0.submitStatus !== undefined &&
unsafeWindow.s0.objectiveQList
) {
submitJsonObj = {
paperDto: JSON.parse(JSON.stringify(unsafeWindow.s0)),
preview: false,
};
} else if (STATE.paperDto) {
submitJsonObj = {
paperDto: JSON.parse(JSON.stringify(STATE.paperDto)),
preview: false,
};
} else {
Dialog.fire({
title: "无法交卷",
html: "ℹ 当前环境下试卷初始化不完整,建议刷新页面后重试。",
confirmText: "知道了",
});
if (btnNode) {
btnNode.disabled = false;
const textSpan = btnNode.querySelector("span");
if (textSpan) textSpan.innerText = "一键交卷";
else btnNode.innerText = "一键交卷";
}
return UI.setStatus("❌ 找不到试卷基础数据,请刷新重试");
}
submitJsonObj.paperDto.submitType = 1;
submitJsonObj.paperDto.switchPageCount = null;
let submitAnswers = [];
let nowTime = Date.now();
const ansQList = answerQuestions;
const origQList = submitJsonObj.paperDto.objectiveQList || [];
for (let i = 0; i < origQList.length; i++) {
let origQ = origQList[i];
let ansQ =
ansQList.find((q) => String(q.id) === String(origQ.id)) || ansQList[i];
if (!ansQ) continue;
let thinkTime = Math.floor(Math.random() * 30000) + 30000;
nowTime += thinkTime;
let qtype = origQ.type;
let qAnswer = {
qid: origQ.id,
type: qtype,
optIds: [],
time: nowTime,
};
if (qtype !== 3) {
(ansQ.correct_indexes || []).forEach((optIdx) => {
const originOpt = origQ.optionDtos?.[optIdx];
if (originOpt && originOpt.id !== undefined) {
qAnswer.optIds.push(originOpt.id);
}
});
} else {
let stdAns = ansQ.answer_text || "";
let finalStr = stdAns.split(" (或) ")[0];
qAnswer.content = { content: finalStr };
}
submitAnswers.push(qAnswer);
}
submitJsonObj.paperDto.answers = submitAnswers;
UI.setStatus("答题卡组装完成,正在向慕课教务交卷...");
const payloadStr = JSON.stringify(submitJsonObj);
GM.xmlHttpRequest({
method: "POST",
url:
"https://www.icourse163.org/web/j/mocQuizRpcBean.submitAnswers.rpc?csrfKey=" +
STATE.csrfKey,
headers: {
"Content-Type": "application/json;charset=UTF-8",
Accept: "application/json",
"Access-Control-Allow-Origin": "*",
Origin: "null",
},
data: payloadStr,
onload: function (resp) {
if (btnNode) {
btnNode.disabled = false;
const textSpan = btnNode.querySelector("span");
if (textSpan) textSpan.innerText = "一键交卷";
else btnNode.innerText = "一键交卷";
}
try {
let r = JSON.parse(resp.responseText);
if ((r.result && r.result === 200) || r.code === 0) {
UI.setStatus("✅ 一键交卷成功!2秒后自动后退返回...");
setTimeout(() => unsafeWindow.history.back(), 2000);
} else {
UI.setStatus(
`❌ 交卷被打回 (服务端返回:${r.result || r.message || "未知异常"})`,
);
}
} catch (e) {
UI.setStatus("❌ 云端反馈数据异常,建议手动检查");
}
},
onerror: function () {
if (btnNode) {
btnNode.disabled = false;
const textSpan = btnNode.querySelector("span");
if (textSpan) textSpan.innerText = "交卷失败重试";
else btnNode.innerText = "交卷失败重试";
}
UI.setStatus("❌ 慕课服务器连接超时或网络异常,请检查拦截插件");
},
});
} catch (e) {
if (btnNode) {
btnNode.disabled = false;
const textSpan = btnNode.querySelector("span");
if (textSpan) textSpan.innerText = "一键交卷";
else btnNode.innerText = "一键交卷";
}
UI.setStatus("⚠ 本地交卷执行脚本发生崩溃");
}
}
function normalizeAnswerPayload(answerData) {
if (answerData && Array.isArray(answerData.questions)) {
return answerData;
}
return null;
}
function renderAnswers(answerData) {
try {
const normalized = normalizeAnswerPayload(answerData);
if (!normalized) {
Dialog.fire({
title: "格式错误",
html: "⚠ 答案数据格式异常,请联系开发者。",
confirmText: "知道了",
});
return UI.setStatus("⚠ 答案数据格式异常");
}
const isNewExam = location.href.includes("newExam");
const warningHint = "请根据理解作答,勿抄参考答案,以免被检测为使用插件!";
const warningHintHtml = `⚠ ${warningHint}
`;
const questions = Array.isArray(normalized.questions) ? normalized.questions : [];
if (questions.length === 0) {
Dialog.toast("⚠ 云端未找到匹配的题目数据");
return UI.setStatus("⚠ 未找到题目数据");
}
let questionRows = [];
if (isNewExam) {
const container = document.querySelector(
".ant-form.ant-form-horizontal",
);
questionRows = container ? Array.from(container.children) : [];
} else {
const listContainer =
document.querySelector(".m-data-lists.f-cb.f-pr.j-data-list") ||
document.querySelector(".m-homeworkQuestionList");
questionRows = listContainer ? Array.from(listContainer.children) : [];
}
let answerTargets;
if (isNewExam) {
answerTargets = document.querySelectorAll(
".index-module__questionInfo__MRpPD",
);
if (answerTargets.length === 0)
answerTargets = document.querySelectorAll("._2LgP5");
} else {
answerTargets = document.querySelectorAll(
".f-richEditorText.j-richTxt",
);
if (answerTargets.length === 0) answerTargets = questionRows;
}
const cardItems = [];
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
const isObjective = Object.prototype.hasOwnProperty.call(q, "id");
const isFillBlank = isObjective && !Array.isArray(q.correct_indexes);
let answerHtml = "";
let cardLabel = "";
if (isObjective) {
if (isFillBlank) {
let stdAns = q.answer_text || "";
answerHtml = ``;
cardLabel = stdAns.substring(0, 12);
} else {
const correctIndexes = Array.isArray(q.correct_indexes)
? q.correct_indexes
: [];
let letters = "";
correctIndexes.forEach((idx) => {
letters += String.fromCharCode(65 + idx);
});
answerHtml = ``;
cardLabel = letters;
}
} else {
answerHtml = `解析:${q.answer_text || "暂无"}
${warningHintHtml}
`;
cardLabel = "主观";
}
if (i < answerTargets.length) {
const richText = answerTargets[i];
if (!STATE.privacyActive) {
richText.insertAdjacentHTML("beforeend", answerHtml);
}
if (isObjective && !isFillBlank) {
const target = i < questionRows.length ? questionRows[i] : richText;
const optionDoms = target.querySelectorAll(
".chooses .u-tbl, ._3m_i-, .m-question-option, .ant-radio-group .ant-radio-wrapper, .ant-checkbox-group .ant-checkbox-wrapper, .choices li",
);
const correctIndexes = Array.isArray(q.correct_indexes)
? q.correct_indexes
: [];
optionDoms.forEach((optNode, idx) => {
if (correctIndexes.includes(idx)) return;
optNode._isWrong = true;
if (STATE.privacyActive) {
const input = optNode.querySelector("input");
if (input) input.disabled = true;
}
});
}
if (isFillBlank) {
const target = i < questionRows.length ? questionRows[i] : richText;
const inputs = target.querySelectorAll(
'input[type="text"], textarea',
);
const stdAns = (q.answer_text || "").split(" (或) ")[0];
inputs.forEach((input) => {
let _copyLock = false;
const doCopy = () => {
if (_copyLock) return;
_copyLock = true;
setTimeout(() => (_copyLock = false), 300);
navigator.clipboard.writeText(stdAns).then(() => {
if (!STATE.privacyActive) {
UI.setStatus(`已复制答案: ${stdAns}`);
}
}).catch(() => { });
};
input.addEventListener("focus", doCopy);
input.addEventListener("click", doCopy);
});
}
}
if (isObjective && !STATE.privacyActive) {
cardItems.push({ idx: i + 1, label: cardLabel });
}
}
if (!window._imAnswerCleanerBound) {
window.addEventListener("hashchange", () => {
document
.querySelectorAll(".neomooc-answer-card, .neomooc_answer, .neomooc-list-score-hint, #neomooc-required-cost")
.forEach((el) => el.remove());
STATE.aid = "";
STATE.testName = "";
UI.setStatus("请打开作业/测验页面并点击获取答案");
});
window._imAnswerCleanerBound = true;
}
document
.querySelectorAll(".neomooc-answer-card")
.forEach((el) => el.remove());
if (cardItems.length === 0) return;
const card = document.createElement("div");
card.className = "neomooc-answer-card";
card.innerHTML =
`📋 解析卡 (${questions.length}题)
` +
cardItems
.map(
(c) =>
`${c.idx}: ${c.label}`,
)
.join("");
document.body.appendChild(card);
card.querySelectorAll(".neomooc-card-item").forEach((item) => {
item.addEventListener("click", () => {
const idx = parseInt(item.dataset.idx) - 1;
if (idx < questionRows.length) {
questionRows[idx].scrollIntoView({
behavior: "smooth",
block: "start",
});
}
});
});
const cardTitle = card.querySelector(".card-title");
let isDragging = false,
startX,
startY,
origLeft,
origTop;
const onMove = (e) => {
if (!isDragging) return;
card.style.left = origLeft + e.clientX - startX + "px";
card.style.top = origTop + e.clientY - startY + "px";
};
const onUp = () => {
isDragging = false;
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
cardTitle.addEventListener("mousedown", (e) => {
isDragging = true;
const rect = card.getBoundingClientRect();
origLeft = rect.left;
origTop = rect.top;
startX = e.clientX;
startY = e.clientY;
card.style.left = origLeft + "px";
card.style.top = origTop + "px";
card.style.right = "auto";
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
});
} catch (e) {
UI.setStatus("⚠ 答案渲染失败: " + e.message);
}
}
async function onClickBrush() {
if (!STATE.verified) return UI.setStatus("请先验证用户");
const btn = document.getElementById("im-btn-brush");
if (btn) btn.disabled = true;
try {
await Dialog.fire({
title: "选择刷课类型",
html: `
`,
confirmText: "",
cancelText: "取消",
onOpen: (modal) => {
const btns = modal.querySelectorAll(".im-btn");
btns.forEach((btnItem) => {
btnItem.onclick = () => {
const type = btnItem.dataset.type;
const labels = {
video: "视频",
doc: "文档/文章",
test: "测验",
discuss: "讨论",
0: "全部",
};
onClickBrushByType(type, labels[type]);
const closeBtn = modal.querySelector(".cancel");
if (closeBtn) closeBtn.click();
};
});
},
});
} finally {
if (btn) btn.disabled = false;
}
}
async function onClickBrushByType(type, label) {
UI.setStatus(`正在分析 ${label} ...`);
try {
const tree = await ensureCourseTree();
const units = parseCourseTree(tree, type);
await submitTargetedTask(units, label);
} catch (e) {
UI.setStatus("❌ 处理异常: " + e);
}
}
async function onClickSlowBrush() {
if (!STATE.verified) return UI.setStatus("请先验证用户");
const btn = document.getElementById("im-btn-slow-brush");
if (btn && btn.disabled) return;
if (STATE.isSlowBrushing) {
stopSlowBrush();
return;
}
if (unsafeWindow.location.href.indexOf("type=detail") === -1) {
Dialog.fire({
title: "无法启动挂机",
html: "⚠ 请先进入课程学习详情页(即显示视频、文档的具体播放页面)后再点击此按钮。",
confirmText: "知道了",
});
return;
}
if (btn) btn.disabled = true;
try {
const ok = await Dialog.fire({
title: "挂机刷课",
html: "挂机刷课将模拟手动点击与播放,由于受浏览器限制,窗口必须保持在前端。确认开始?",
confirmText: "开始挂机",
});
if (ok) startSlowBrush();
} finally {
if (btn) btn.disabled = false;
}
}
let slowBrushTimer = null;
let lastUrl = "";
async function loadFlatUnits() {
if (STATE.flatUnits.length > 0) return STATE.flatUnits;
try {
const tree = await fetchCourseTree(STATE.csrfKey, STATE.termId);
const list = [];
(tree?.chapters || []).forEach((c) =>
(c.lessons || []).forEach((l) =>
(l.units || []).forEach((u) => {
list.push({ ...u, lessonId: l.id });
}),
),
);
STATE.flatUnits = list;
return list;
} catch (e) {
return [];
}
}
async function startSlowBrush() {
STATE.isSlowBrushing = true;
const btn = document.getElementById("im-btn-slow-brush");
if (btn) {
btn.textContent = "停止挂机";
btn.classList.add("danger");
}
UI.setStatus("🚀 挂机刷课中,请勿遮挡浏览器");
lastUrl = "";
await loadFlatUnits();
slowBrushTimer = setInterval(() => {
const currentUrl = unsafeWindow.location.href;
if (currentUrl.indexOf("learn/content") === -1) {
return stopSlowBrush();
}
if (currentUrl.indexOf("type=detail") === -1) return;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
STATE.isJumping = false;
handleCurrentUnit();
return;
}
if (STATE.isJumping) return;
const video = document.querySelector("video");
if (video) {
if (video.paused && !video.ended) video.play().catch(() => { });
video.muted = true;
if (!video._imBound) {
video._imBound = true;
video.addEventListener("pause", () => {
if (STATE.isSlowBrushing && !video.ended)
video.play().catch(() => { });
});
video.addEventListener("ended", () => {
if (STATE.isSlowBrushing) {
UI.setStatus("🎬 播放结束,准备跳转...");
setTimeout(gotoNextUnit, 1000);
}
});
}
if (video.ended || document.querySelector(".playEnd.f-f0.f-pa")) {
gotoNextUnit();
}
}
const pptViewer = document.querySelector(".ux-edu-pdfthumbnailviewer");
if (pptViewer) handlePPT(pptViewer);
}, 1500);
}
function stopSlowBrush() {
STATE.isSlowBrushing = false;
clearInterval(slowBrushTimer);
const btn = document.getElementById("im-btn-slow-brush");
if (btn) {
btn.textContent = "挂机刷课";
btn.classList.remove("danger");
}
UI.setStatus("⏹ 挂机已停止");
}
function handleCurrentUnit() {
const curIdMatch = unsafeWindow.location.href.match(/cid=(\d+)/);
const curId = curIdMatch ? curIdMatch[1] : null;
let typeStr = "分析中...";
if (curId && STATE.flatUnits.length > 0) {
const unit = STATE.flatUnits.find((u) => u.id.toString() === curId);
if (unit) {
const types = {
1: "视频",
3: "文档",
4: "富文本",
5: "测验",
6: "讨论",
};
typeStr = types[unit.contentType] || "未知单元";
}
} else {
const tab = document.querySelector(".u-learnBCUI .f-cb .current");
if (tab) {
typeStr = tab.innerText.trim();
}
}
UI.setStatus(`正在挂机: ${typeStr}`);
if (
STATE.flatUnits.length > 0 &&
(typeStr === "富文本" || typeStr === "讨论" || typeStr === "测验")
) {
UI.setStatus(`${typeStr} 单元,5秒后自动跳过...`);
setTimeout(() => {
if (unsafeWindow.location.href.includes(`cid=${curId}`)) {
gotoNextUnit();
}
}, 5000);
}
}
function handlePPT(viewer) {
if (STATE._isPptHandling) return;
STATE._isPptHandling = true;
const links = viewer.querySelectorAll("a");
let currentIdx = 0;
const footerInput = document.querySelector(
".ux-h5pdfreader_container_footer_pages_in",
);
if (footerInput) {
currentIdx = parseInt(footerInput.value) - 1;
}
async function clickNext(idx) {
if (!STATE.isSlowBrushing) {
STATE._isPptHandling = false;
return;
}
if (idx >= 0 && idx < links.length && links[idx]) {
links[idx].click();
UI.setStatus(`文 档: ${idx + 1}/${links.length}`);
const wait = 2500 + Math.random() * 2000;
setTimeout(() => clickNext(idx + 1), wait);
} else {
STATE._isPptHandling = false;
if (links.length > 0 && idx >= links.length) {
UI.setStatus("文 档已阅读完毕,准备跳转...");
setTimeout(gotoNextUnit, 1000);
} else if (links.length === 0) {
UI.setStatus("文 档: 正在载入内容...");
} else {
}
}
}
clickNext(currentIdx);
}
function gotoNextUnit() {
if (!STATE.isSlowBrushing || STATE.isJumping) return;
const loc = unsafeWindow.location.href;
const currentItem = document.querySelector(".f-fl.current");
if (!currentItem) return;
STATE.isJumping = true;
let next = currentItem.nextElementSibling;
if (!next && currentItem.parentElement) {
findAndJump(loc);
} else if (next) {
next.click();
}
}
async function findAndJump(loc) {
if (!STATE.isSlowBrushing) return;
UI.setStatus("查找下一单元...");
try {
await loadFlatUnits();
const curIdMatch = loc.match(/cid=(\d+)/);
const curId = curIdMatch ? curIdMatch[1] : null;
if (!curId) return UI.setStatus("⚠ 无法识别当前页面位置");
const currentIndex = STATE.flatUnits.findIndex(
(u) => u.id.toString() === curId,
);
let nextUnit = null;
if (currentIndex !== -1 && currentIndex + 1 < STATE.flatUnits.length) {
nextUnit = STATE.flatUnits[currentIndex + 1];
}
if (!nextUnit) {
nextUnit = STATE.flatUnits.find((u) => (u.completePercent || 0) < 0.8);
}
if (!nextUnit) {
stopSlowBrush();
Dialog.toast("🎉 恭喜,当前课程所有单元已刷完!");
return;
}
const nextUrl =
loc.split("#")[0] +
`#/learn/content?type=detail&id=${nextUnit.lessonId || 0}&cid=${nextUnit.id}`;
UI.setStatus(`即将跳转到: ${nextUnit.name}`);
setTimeout(() => {
if (STATE.isSlowBrushing) unsafeWindow.location.href = nextUrl;
}, 1000);
} catch (e) {
UI.setStatus("⚠ 自动跳转失败: " + (e.message || e));
}
}
async function onClickCloud() {
if (!STATE.verified) return UI.setStatus("请先验证用户");
const btn = document.getElementById("im-btn-cloud");
const oldText = btn ? btn.innerText : "任务列表";
if (btn) {
btn.disabled = true;
btn.innerText = "获取中...";
}
UI.setStatus("🚀 正在获取任务与详细账单...");
try {
const res = await API.listTasks(
STATE.user.id,
CONFIG.use_cdkey ? CONFIG.cdkey : "",
);
if (res.status === 0) {
if (res.score !== undefined) {
UI.updateScore(res.score);
STATE.user.score = res.score;
}
const statusMap = {
pending: "排队中",
executed: "同步中",
completed: "已完成",
failed: "已完成",
cancelled: "已取消",
};
const tasksHtml =
res.tasks.length > 0
? res.tasks
.map(
(t) => `
进度: ${t.progress || 0}%
已用: ${t.pointUsed || 0}积分
${t.currentUnitName ? `
当前: ${t.currentUnitName}
` : ""}
`,
)
.join("")
: '暂无云端任务
';
const logsHtml =
res.logs && res.logs.length > 0
? res.logs
.map(
(log) => `
${log.reason || "未说明原因"}
${new Date(log.createTime).toLocaleString()}
${log.amount <= 0 ? (log.amount === 0 ? "-0" : log.amount) : "+" + log.amount}
`,
)
.join("")
: '暂无积分变动记录
';
Dialog.fire({
title: "云端管理中心",
cancelText: "",
confirmText: "我知道了",
html: `
● 任务概览 (最近5条)
${tasksHtml}
`,
});
UI.setStatus("✅ 云端数据加载成功");
} else {
UI.setStatus("❌ " + (res.msg || "查询失败"));
}
} catch (e) {
Dialog.fire({
title: "查询失败",
html: "⚠ 查询任务或积分异常,可能是云端服务暂时不可用。",
confirmText: "知道了",
});
UI.setStatus("⚠ 查询数据失败");
} finally {
if (btn) {
btn.disabled = false;
btn.innerText = oldText;
}
}
}
async function onClickConfig() {
const btn = document.getElementById("im-btn-config");
if (btn) btn.disabled = true;
try {
const result = await Dialog.fire({
title: "功能配置",
html: `
使用CDKey
`,
confirmText: "保存并刷新",
onOpen: (modal) => {
const check = modal.querySelector("#im-cfg-use-cdkey");
const input = modal.querySelector("#im-cfg-cdkey");
if (check) {
check.onchange = () => {
input.disabled = !check.checked;
};
}
},
onConfirm: () => ({
cdkey: document.getElementById("im-cfg-cdkey").value.trim(),
use_cdkey: document.getElementById("im-cfg-use-cdkey").checked,
privacyMode: document.getElementById("im-cfg-privacy-mode").checked,
}),
});
if (result) {
const cdkeyChanged =
CONFIG.cdkey !== result.cdkey || CONFIG.use_cdkey !== result.use_cdkey;
CONFIG.cdkey = result.cdkey;
CONFIG.use_cdkey = result.use_cdkey;
GM_setValue("cdkey", CONFIG.cdkey);
GM_setValue("use_cdkey", CONFIG.use_cdkey);
if (CONFIG.privacyMode !== result.privacyMode) {
togglePrivacyMode(result.privacyMode);
}
if (cdkeyChanged) {
UI.setStatus("配置已保存,正在重新验证权限...");
onClickRefreshScore();
} else {
UI.setStatus("✅ 配置已保存");
}
}
} finally {
if (btn) btn.disabled = false;
}
}
async function onClickHelp() {
const btn = document.getElementById("im-help-btn");
if (btn && btn.getAttribute("disabled")) return;
if (btn) btn.setAttribute("disabled", "true");
try {
await Dialog.fire({
title: "NeoMooc 助手使用指引",
confirmText: "我理解了",
cancelText: "",
html: `
隐私安全与辅助功能
- 快捷控制:按 Esc 键可开关脚本主面板。
- 隐私过滤机制:开启隐私模式后,系统将自动隐藏全部面板与答案;在此模式下,仅允许勾选正确答案。
测验与作业答题辅助
进入测验或作业页面后,点击面板上的
「获取答案」或按快捷键
Alt+A:
- 题目解析显示:系统将自动匹配云端最高评分答案;若当前题目在云端尚无收录,系统将实时调用 AI 模型进行深度分析,并提供逻辑推导出的最优解。
- 快速填充:针对填空题,系统检测到光标聚焦时,点击输入框即可通过内部剪贴板机制快速粘贴标准答案。
- 答题卡:页面左侧/右侧会生成悬浮答题卡,标记已捕获的题目序号,支持点击快速跳转定位。
刷课模式对比与说明
本助手提供两种截然不同的刷课方案,用户可按需选择:
-
云端代看(推荐):由后端服务器接管,支持视频、文档、测验、讨论等全类型单元。提交任务后可立即关闭浏览器,任务将在云端代刷,通过「任务列表」查看积分扣除明细与同步状态。
-
本地挂机模拟:在播放页启动后,脚本将通过模拟前端操作完成进度。必须保持浏览器视窗常驻前台(不可最小化或被其他窗口遮挡),适用于无需消耗额外积分的本地自动化。
-
定向范围刷课:将鼠标移动到课程目录树,可看到针对特定章节、小节、甚至单个视频单元的独立控制按钮。
积分体系与计费规则
本助手采用积分制管理云端资源调用,具体规则如下:
- 获取答案扣费:普通作业或测验每次消耗 10 积分;考试每次消耗 50 积分。
- 云端刷课扣费:每一个学习单元(视频、文档、讨论等)消耗 1 积分。单次提交任务最高封顶扣除 30 积分,超出部分不再计费。
- 会员尊享权益:VIP 会员在有效期内享有无限额度,所有云端功能均不消耗积分(不可跨账号使用)。
- 充值后发放CDKey,配置后可跨账号使用。
`,
});
} finally {
if (btn) btn.removeAttribute("disabled");
}
}
Object.assign(CONFIG, {
panelWidth: 320,
cdkey: GM_getValue("cdkey", ""),
use_cdkey: GM_getValue("use_cdkey", true),
theme: GM_getValue("theme", "dark"),
privacyMode: GM_getValue("privacy_mode", false),
});
STATE.privacyActive = CONFIG.privacyMode;
togglePrivacyMode(STATE.privacyActive);
init();
})();