// ==UserScript== // @name NeoMooc|中国大学Mooc|AI答题|云端刷课|挂机刷课|自带题库 // @namespace http://neomooc.click/ // @version 1.0.7 // @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 neomooc.click // @connect icourse163.org // @connect localhost // @connect 127.0.0.1 // @license Proprietary // @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, cloudAvailable: true, }; 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); }); } }); }, }; 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); --im-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); } [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); --im-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); } #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; box-shadow: var(--im-shadow); 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); box-shadow: 0 4px 12px rgba(0,0,0,0.2); 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; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); 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; box-shadow: 0 4px 20px rgba(0,0,0,0.15); 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; box-shadow: 0 24px 80px rgba(0,0,0,0.5); 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; box-shadow: 0 0 0 3px rgba(88,166,255,0.15); } .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; box-shadow: 0 2px 8px rgba(88,166,255,0.3); } .im-dialog-btn.confirm:hover { filter: brightness(1.1); transform: translateY(-1px); } .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; box-shadow: 0 2px 4px rgba(0,0,0,0.1); 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; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); cursor: pointer; transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); letter-spacing: 0.8px; text-transform: uppercase; } .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); box-shadow: 0 15px 45px rgba(0, 0, 0, 0.35); } .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); } `); 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 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", "mousedown", "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 extractPageInfo() { const w = unsafeWindow; const info = { termId: "", courseId: "", csrfKey: document.cookie.match(/NTESSTUDYSI=(.*?)(;|$)/)?.[1] || "", }; if (w.moocTermDto) info.termId = String(w.moocTermDto.id || ""); else if (w.termDto) info.termId = String(w.termDto.id || ""); if (w.courseDto) info.courseId = String(w.courseDto.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(/\/learn\/([^\/?#&]+)/); if (pathMatch) info.courseId = pathMatch[1]; } return info; } 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: 10000, 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: 10000, 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, objectiveQList, subjectiveQList, ) { const data = maskParams({ userId, verify, tid, aid, cdkey, objectiveQList, subjectiveQList, }); 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 || ""}`, ); }, }; 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 = `
⚡ NeoMooc v${GM_info.script.version}
${ICONS.help} ${CONFIG.theme === "dark" ? ICONS.moon : ICONS.sun} ×
STATUS 等待操作...
题库总量
--
×
`; if (document.body) { document.body.appendChild(div); } else { document.documentElement.appendChild(div); } this.panel = div; this._bindHeaderActions(); this._bindDrag(); }, _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() { CONFIG.serverUrl = "https://neomooc.click/neomooc"; window.actualServerUrl = CONFIG.debug ? "http://127.0.0.1:5000" : CONFIG.serverUrl; let isDragging = false, startX, startY, origLeft, origTop; this.panel.addEventListener("mousedown", (e) => { 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) { document.getElementById("im-user").textContent = id || name; }, 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(realCount) { STATE.qCount = realCount; document.getElementById("im-qcount").textContent = realCount; }, 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; } async function submitTargetedTask(units, entityName) { if (units.length === 0) { Dialog.fire({ title: "无需操作", html: "⚠ 所有单元已完成,无需继续刷课。", confirmText: "知道了", }); return UI.setStatus("⚠ 所有单元已全部完成"); } const maxTaskCost = STATE.maxTaskCost || 30; const estimatedCost = Math.min(units.length, maxTaskCost); const capNote = units.length > maxTaskCost ? `
(单次任务封顶 ${maxTaskCost} 积分)` : ""; const ok = await Dialog.fire({ title: "定向刷课确认", html: `确定要刷 ${entityName} 吗?

将提交 ${units.length} 个单元,预计消耗 ${estimatedCost} 积分。${capNote}`, 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; if (!STATE.termId || !STATE.csrfKey) { return UI.setStatus("❌ 提取课程信息失败 (termId),请稍后重试或刷新页面"); } await enrichUnitsWithPageCount(units); const courseName = document.title.split("-")[0].trim(); const res = await API.submitTask( STATE.user.id, STATE.termId, STATE.courseId, courseName, STATE.csrfKey, "0", units, CONFIG.use_cdkey ? CONFIG.cdkey : "", ); if (res.status === 0) { UI.setStatus(`✅ 任务已提交,系统会自动处理,请耐心等待。`); Dialog.fire({ title: "提交成功", html: `✅ 任务已开始!

您可以关闭这个页面,或者继续去刷其他的课,进度会自动同步。`, confirmText: "知道了", }); } else UI.setStatus(`❌ ${res.msg}`); } function observe() { const targetNode = document.getElementById("courseLearn-inner-box"); if (!targetNode) return setTimeout(observe, 1000); 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.fire({ title: "无需刷课", html: "⚠ 该单元已达到 80% 完成度。", confirmText: "知道了", }); 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); } } }; const observer = 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); observer.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, STATE.user.id); UI.updateScore(res.score); UI.updateVip(res.vip_expire_time); if (res.announce) UI.updateNotice(res.announce); if (res.q_count !== undefined) UI.updateQCount(res.q_count); 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(); if (GM_getValue("first_load_v3", true)) { setTimeout(() => { onClickHelp(); GM_setValue("first_load_v3", false); }, 2000); } } 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); } 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); if (res.q_count !== undefined) UI.updateQCount(res.q_count); 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.fire({ title: "未检测到试卷", html: "⚠ 未从当前页面检测到有效的测验内容,请先打开作业或考试页面。", confirmText: "知道了", }); UI.setStatus("⚠ 未检测到试卷"); } 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 : "", STATE.paperDto?.objectiveQList, STATE.paperDto?.subjectiveQList, ); if (res.status === 0) { UI.updateScore(res.score); STATE.user.score = res.score; renderAnswers(res.answer); 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 : "", STATE.paperDto?.objectiveQList, STATE.paperDto?.subjectiveQList, ); 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 results = res.answer?.results || res.answer?.result || res.answer; const answerPaper = results?.mocPaperDto || results; if (!answerPaper || !answerPaper.objectiveQList) { 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 = answerPaper.objectiveQList || []; const origQList = submitJsonObj.paperDto.objectiveQList || []; for (let i = 0; i < origQList.length; i++) { let origQ = origQList[i]; let ansQ = ansQList.find((q) => q.id === 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.optionDtos || []).forEach((opt) => { if (opt.answer) qAnswer.optIds.push(opt.id); }); } else { let stdAns = ansQ.stdAnswer || ""; let finalStr = stdAns.split("##%_YZPRLFH_%##")[0].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 renderAnswers(answerData) { try { const results = answerData?.results || answerData?.result || answerData; const paper = results?.mocPaperDto || results; if (!paper) { Dialog.fire({ title: "格式错误", html: "⚠ 答案数据格式异常,请联系开发者。", confirmText: "知道了", }); return UI.setStatus("⚠ 答案数据格式异常"); } const isNewExam = location.href.includes("newExam"); const objList = paper.objectiveQList || []; const subList = paper.subjectiveQList || []; objList.forEach((q) => (q._isObj = true)); subList.forEach((q) => (q._isSub = true)); const questions = [...objList, ...subList]; if (questions.length === 0) { Dialog.fire({ title: "未找到题目", html: "⚠ 云端未找到匹配的题目数据。", confirmText: "知道了", }); 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 qType = q.type; let answerHtml = ""; let cardLabel = ""; if (q._isObj) { if (qType === 3) { let stdAns = (q.stdAnswer || "").replace( /##%_YZPRLFH_%##/g, " (或) ", ); answerHtml = `
解析:
${stdAns}
`; cardLabel = stdAns.substring(0, 12); } else { const options = q.optionDtos || []; let letters = "", detail = ""; options.forEach((opt, idx) => { if (opt.answer) { const L = String.fromCharCode(65 + idx); letters += L; detail += `
${L}.
${opt.content}
`; } }); answerHtml = `
解析:
${detail}
`; cardLabel = letters; } } else if (q._isSub) { const judges = (q.judgeDtos || []) .map((j) => j.msg || j.content || "") .filter(Boolean); answerHtml = `
解析:
${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.courseName || t.termId} ${statusMap[t.status] || "未知"}
进度: ${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}
积分消费流水
${logsHtml}
`, }); 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: `
`, 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(); })();