// ==UserScript==
// @name NeoMooc【内测期免费🔥】|中国大学慕课|AI答题|云端刷课|挂机刷课|自带题库
// @namespace http://tampermonkey.net/
// @version 1.0.3
// @description 中国大学慕课答题、云端刷课助手。自带题库、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 MIT
// @require https://scriptcat.org/lib/637/1.4.8/ajaxHooker.js#sha256=dTF50feumqJW36kBpbf6+LguSLAtLr7CEs3oPmyfbiM=
// @run-at document-start
// @icon https://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;
}
ajaxHooker.hook((request) => {
if (request.url.includes("saveDraftAnswers")) {
request.abort = true;
return;
}
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;
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,
};
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 = `
解析:${judges.join(";") || q.analyse || "暂无"}
`;
cardLabel = "主观";
}
if (i < answerTargets.length) {
const richText = answerTargets[i];
if (!STATE.privacyActive) {
richText.insertAdjacentHTML("beforeend", answerHtml);
}
if (q._isObj && qType !== 3) {
const target = i < questionRows.length ? questionRows[i] : richText;
const options = q.optionDtos || [];
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",
);
options.forEach((opt, idx) => {
if (idx < optionDoms.length) {
const optNode = optionDoms[idx];
if (!opt.answer) {
optNode._isWrong = true;
if (STATE.privacyActive) {
const input = optNode.querySelector("input");
if (input) input.disabled = true;
}
}
}
});
}
if (qType === 3) {
const target = i < questionRows.length ? questionRows[i] : richText;
const inputs = target.querySelectorAll(
'input[type="text"], textarea',
);
const stdAns = (q.stdAnswer || "")
.split("##%_YZPRLFH_%##")[0]
.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 (q._isObj && !STATE.privacyActive) {
cardItems.push({ idx: i + 1, label: cardLabel, type: qType });
}
}
if (!window._imAnswerCleanerBound) {
window.addEventListener("hashchange", () => {
document
.querySelectorAll(".neomooc-answer-card, .neomooc_answer")
.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.fire({
title: "挂机完成",
html: "恭喜,当前课程所有单元已刷完。",
});
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();
})();