// ==UserScript== // @name 超星作业题库提取器 // @namespace http://tampermonkey.net/ // @version 2.2 // @description 提取超星作业答案,升级为个人题库管理器。全面突破自建题库抓取;修复大题库遗漏与分页问题;支持双模式TXT导出;支持反反调试、未考题目强制抓取。兼容新旧版UI。仍有少量bug,修累了 // @author 毫厘 // @match *://*.chaoxing.com/* // @match *://*.edu.cn/* // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // 保持数据库 KEY 不变以免清空老用户的已有数据 const DB_KEY = 'cx_db_v2.0'; const AUTO_EXTRACT_KEY = 'cx_auto_ext_v2.0'; const FETCH_ALL_KEY = 'cx_fetch_all_v2.0'; const ANTI_DEBUG_KEY = 'cx_anti_debug_v2.0'; const COM_NOTIFY = 'cx_com_notify_v2'; const COM_RENDER = 'cx_com_render_v2'; const isTopWindow = (window === window.top); // ========================================================= // 0. 网页反调试拦截 (必须最先执行) // ========================================================= if (GM_getValue(ANTI_DEBUG_KEY, false)) { try { const _window = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; console.log("[反反调试脚本] 已在最底层注入,准备拦截 debugger..."); const originalConstructor = _window.Function.prototype.constructor; _window.Function.prototype.constructor = function(...args) { if (args && args.length > 0) { const code = args[args.length - 1]; if (typeof code === 'string' && code.includes('debugger')) { return function() {}; } } return originalConstructor.apply(this, args); }; const originalEval = _window.eval; _window.eval = function(code) { if (typeof code === 'string' && code.includes('debugger')) { code = code.replace(/debugger/ig, ''); } return originalEval.call(this, code); }; const originalSetInterval = _window.setInterval; _window.setInterval = function(func, delay, ...args) { if (typeof func === 'string' && func.includes('debugger')) { func = func.replace(/debugger/ig, ''); } else if (typeof func === 'function' && func.toString().includes('debugger')) { func = function() {}; } return originalSetInterval.call(this, func, delay, ...args); }; const originalSetTimeout = _window.setTimeout; _window.setTimeout = function(func, delay, ...args) { if (typeof func === 'string' && func.includes('debugger')) { func = func.replace(/debugger/ig, ''); } else if (typeof func === 'function' && func.toString().includes('debugger')) { func = function() {}; } return originalSetTimeout.call(this, func, delay, ...args); }; } catch (e) { console.error("Anti-Debug Inject Failed: ", e); } } // ========================================================= // 1. 样式注入 (原生按钮样式 + 主面板样式) // ========================================================= const injectStyles = setInterval(() => { if (document.head) { clearInterval(injectStyles); GM_addStyle(` #cx-export-btn { display: inline-block; padding: 0 15px; background: #6f42c1; border-radius: 35px; line-height: 32px; color: #fff; font-size: 14px; cursor: pointer; margin-left: 10px; margin-right: 10px; border: none; font-weight: bold; transition: all 0.3s; box-shadow: 0 2px 6px rgba(111,66,193,0.3); vertical-align: middle; z-index: 999; } #cx-export-btn:hover:not(:disabled) { background: #59339d; transform: translateY(-1px); } #cx-export-btn:disabled { background: #adb5bd; cursor: not-allowed; box-shadow: none; transform: none; } #extractQuizButtonGM_merged { position: fixed; top: 160px; right: 20px; z-index: 2147483647; padding: 10px 15px; background-color: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(40,167,69,0.4); transition: all 0.3s; } #extractQuizButtonGM_merged:hover:not(:disabled) { background-color: #218838; } #extractQuizButtonGM_merged:disabled { background-color: #5a6268; cursor: not-allowed; } #cx-qb-float-btn { position: fixed; right: 20px; top: 100px; z-index: 2147483647; background: #007bff; color: white; border: none; padding: 12px 20px; border-radius: 50px; cursor: pointer; font-size: 14px; font-weight: bold; box-shadow: 0 4px 12px rgba(0,123,255,0.4); transition: all 0.3s; display: flex; align-items: center; gap: 8px; } #cx-qb-float-btn:hover { background: #0056b3; transform: scale(1.05); } #cx-qb-main-panel { position: fixed; top: 80px; right: 20px; width: 420px; max-height: 85vh; background: #f8f9fa; z-index: 2147483647; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.25); display: none; flex-direction: column; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; border: 1px solid #dee2e6; } .cx-qb-header { background: #fff; padding: 12px 20px; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .cx-qb-title { margin: 0; font-size: 16px; color: #212529; font-weight: bold; display: flex; align-items: center; gap: 8px; } .cx-qb-controls { display: flex; gap: 10px; } .cx-qb-icon-btn { background: transparent; border: none; font-size: 16px; color: #6c757d; cursor: pointer; padding: 4px; border-radius: 4px; transition: background 0.2s; display: flex; align-items: center; justify-content: center;} .cx-qb-icon-btn:hover { background: #e9ecef; color: #212529; } .cx-qb-content { flex: 1; overflow-y: auto; background: #f8f9fa; position: relative; min-height: 300px; } .cx-qb-view-list { padding: 15px; } .cx-qb-task-item { background: #fff; border: 1px solid #e9ecef; border-radius: 8px; padding: 15px; margin-bottom: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); transition: box-shadow 0.2s; } .cx-qb-task-item:hover { box-shadow: 0 4px 10px rgba(0,0,0,0.08); border-color: #dee2e6; } .cx-qb-task-info { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; } .cx-qb-task-info h4 { margin: 0; color: #343a40; font-size: 15px; flex: 1; word-break: break-all; line-height: 1.4; } .cx-qb-task-info span { font-size: 12px; background: #e9ecef; color: #495057; padding: 2px 8px; border-radius: 20px; margin-left: 10px; white-space: nowrap; } .cx-qb-task-actions { display: flex; gap: 8px; flex-wrap: wrap; } .cx-qb-task-actions button { flex: 1; padding: 6px 0; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; transition: background 0.2s; } .btn-view { background: #e2eafc; color: #3b5bdb; } .btn-view:hover { background: #c5d5fc; } .btn-exp-txt { background: #e6fcf5; color: #0ca678; } .btn-exp-txt:hover { background: #c3fae8; } .btn-exp-json { background: #fff3bf; color: #f59f00; } .btn-exp-json:hover { background: #ffec99; } .btn-del { background: #ffe3e3; color: #c92a2a; flex: 0.5; } .btn-del:hover { background: #ffc9c9; } .cx-qb-view-detail { padding: 15px; display: none; background: #fff; min-height: 100%; } .cx-detail-header { display: flex; align-items: center; gap: 12px; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px dashed #e9ecef; } .cx-detail-header button { padding: 6px 12px; border: none; background: #f1f3f5; border-radius: 6px; cursor: pointer; font-weight: bold; color: #495057; } .cx-detail-header button:hover { background: #e9ecef; } .cx-detail-header h3 { margin: 0; font-size: 15px; color: #212529; flex: 1; word-break: break-all; } .cx-q-item { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 1px solid #e9ecef; } .cx-q-raw { white-space: pre-wrap; font-size: 13px; color: #343a40; line-height: 1.6; font-family: Consolas, "Courier New", monospace; } .cx-qb-view-settings { padding: 20px; display: none; background: #fff; min-height: 100%; } .cx-setting-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; margin-bottom: 15px; } .cx-setting-info h4 { margin: 0 0 5px 0; color: #212529; font-size: 15px; } .cx-setting-info p { margin: 0; color: #868e96; font-size: 12px; } .cx-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0;} .cx-switch input { opacity: 0; width: 0; height: 0; } .cx-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; } .cx-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .cx-slider { background-color: #28a745; } input:checked + .cx-slider:before { transform: translateX(20px); } .cx-qb-footer { background: #fff; padding: 12px 15px; border-top: 1px solid #e9ecef; display: flex; justify-content: space-between; } .cx-qb-footer button { padding: 6px 10px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; color: #495057; background: #f1f3f5; font-weight: bold; } .cx-qb-footer button:hover { background: #e9ecef; } .btn-g-clear { color: #c92a2a !important; background: #ffe3e3 !important; } .btn-g-clear:hover { background: #ffc9c9 !important; } .cx-empty-state { text-align: center; color: #adb5bd; padding: 50px 20px; font-size: 14px; line-height: 1.6; } #cx-top-notification { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); color: white; padding: 12px 25px; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.15); z-index: 2147483647; font-size: 14px; display: none; text-align: center; font-weight: bold; transition: opacity 0.3s; pointer-events:none; } `); } }, 50); // ========================================================= // 2. 本地数据库逻辑 // ========================================================= function getDB() { let defaultDB = { version: "2.0", tasks: {} }; let data = GM_getValue(DB_KEY, null); if (data) { try { return JSON.parse(data); } catch(e) { return defaultDB; } } return defaultDB; } function saveDB(db) { GM_setValue(DB_KEY, JSON.stringify(db)); } function mergeQuestions(oldArr, newArr) { let map = new Map(); oldArr.forEach(q => map.set(q.pureQText, q)); newArr.forEach(q => { let key = q.pureQText; if (!map.has(key)) map.set(key, q); else { let existing = map.get(key); if (existing.answer.includes("未能提取") || existing.answer.includes("暂未找到")) { if (!q.answer.includes("未能提取") && !q.answer.includes("暂未找到")) map.set(key, q); } } }); return Array.from(map.values()); } function addTaskToDB(taskTitle, questions) { if (!questions || questions.length === 0) return 0; let db = getDB(); let safeTitle = taskTitle || `未命名提取_${new Date().toLocaleString()}`; if (!db.tasks[safeTitle]) db.tasks[safeTitle] =[]; let oldLen = db.tasks[safeTitle].length; db.tasks[safeTitle] = mergeQuestions(db.tasks[safeTitle], questions); saveDB(db); return db.tasks[safeTitle].length - oldLen; } function deleteTaskFromDB(taskTitle) { let db = getDB(); if (db.tasks[taskTitle]) { delete db.tasks[taskTitle]; saveDB(db); } } function notifyTopWindow(message, isError = false) { GM_setValue(COM_NOTIFY, { msg: message, isError: isError, t: Date.now() + Math.random() }); } function requestTopRender() { GM_setValue(COM_RENDER, Date.now() + Math.random()); } // ========================================================= // 3. 强力抓取解析引擎 // ========================================================= function normalizeText(text) { return (typeof text !== 'string') ? "" : text.replace(/\s+/g, ' ').trim(); } function normalizeQuestionText(rawText) { if (!rawText) return ""; let text = rawText.replace(/^【.*?题】\s*/, '').trim(); text = text.replace(/\s*[(\(]\s*\d+(\.\d+)?\s*分\s*[)\)]\s*$/, '').trim(); text = text.replace(/\(\s*( |\s)*\)/g, '( )').replace(/(\s*( |\s)*)/g, '( )'); return normalizeText(text); } function getCleanTextFromElement(element) { if (!element) return ""; const clone = element.cloneNode(true); clone.querySelectorAll('script, style').forEach(el => el.remove()); let html = clone.innerHTML.replace(/]*>/gi, '\n').replace(/<\/p>/gi, '\n').replace(/]*>/gi, '\n').replace(/]*>/gi, '\n').replace(/<\/div>/gi, '\n'); const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; return normalizeText(tempDiv.textContent || tempDiv.innerText || "").replace(/\n\s*\n/g, '\n').trim(); } function coreExtract(context) { let questions =[]; let seenTexts = new Set(); function addQ(qObj) { if (!qObj || !qObj.pureQText || qObj.pureQText === "未知题目") return; if (!seenTexts.has(qObj.pureQText)) { seenTexts.add(qObj.pureQText); questions.push(qObj); } } let markItems = context.querySelectorAll('.mark_item'); if (markItems.length > 0) { markItems.forEach(item => { let typeName = "未知题型"; let typeHeader = item.querySelector('.type_tit, .mark_name h3, .question_title'); if (typeHeader) typeName = detectTypeName(normalizeText(typeHeader.textContent).replace(/^[一二三四五六七八九十]+[、.]\s*/, '').replace(/\(.*?\)$/, '').trim()); item.querySelectorAll('.questionLi').forEach(q => addQ(processFanyaQuestion(q, typeName))); }); } if (questions.length === 0) { context.querySelectorAll('.questionLi').forEach(q => addQ(processFanyaQuestion(q, "未知题型"))); } let zyContainers = context.querySelectorAll('.CyBottom, .ZyBottom, #ZyBottom'); if (zyContainers.length > 0) { zyContainers.forEach(element => { let currentType = "未知题型"; Array.from(element.children).forEach(child => { const isHeader = child.classList.contains('Cy_TItle1') || child.classList.contains('newTestType') || (child.tagName === 'H3' && child.className.includes('TestType')) || child.querySelector('.newTestType'); if (isHeader) { currentType = detectTypeName(normalizeText(child.textContent).replace(/^[一二三四五六七八九十]+[、.]\s*/, '').replace(/\(.*?\)$/, '').replace(/\(.*\)$/, '').trim()); } else if (child.classList.contains('TiMu')) { addQ(processZyBottomQuestion(child, currentType)); } else { child.querySelectorAll('.TiMu').forEach(q => addQ(processZyBottomQuestion(q, currentType))); } }); }); } if (questions.length === 0) { context.querySelectorAll('.TiMu').forEach(q => addQ(processZyBottomQuestion(q, "未知题型"))); } return questions; } function processZyBottomQuestion(qEl, typeName) { let qOutput = ""; let pureQText = "未知题目"; let finalAns = "未能提取答案"; try { const titleDiv = qEl.querySelector('.Cy_TItle, .Zy_TItle, .question_title, h3'); let qText = "题干未能提取"; if (titleDiv) { const contentEl = titleDiv.querySelector('.qtContent, div.clearfix, .question_text') || titleDiv; if(contentEl) qText = normalizeQuestionText(getCleanTextFromElement(contentEl)); } pureQText = qText.replace(/^\d+[\.、]\s*/, ''); qOutput += qText + "\n"; qEl.querySelectorAll('.Cy_ulTop li, .Zy_ulTop li, .mark_letter li, .options li, .stem_answer li').forEach(opt => { const label = opt.querySelector('i.fl, i.num')?.textContent.trim() || ""; let text = ""; const anchor = opt.querySelector('a'); if (anchor) text = normalizeText(anchor.textContent); else { let clone = opt.cloneNode(true); clone.querySelector('i.fl, i.num')?.remove(); text = normalizeText(clone.textContent); } qOutput += label + " " + text + "\n"; }); let found = false; const newBx = qEl.querySelector('.newAnswerBx'); if (newBx) { const rightNode = newBx.querySelector('.correctAnswerBx .answerCon, .correctAnswer .answerCon'); if (rightNode) { finalAns = normalizeText(rightNode.innerText); found = true; } if (!found) { const multiBlankBx = newBx.querySelector('.myAllAnswerBx'); if (multiBlankBx) { let tempAnswers =[], hasWrong = false; multiBlankBx.querySelectorAll('.myAnswerBx').forEach(sub => { const ansContent = sub.querySelector('.myAnswer'); if (ansContent) { let t = normalizeText(ansContent.innerText).replace(/第[一二三四五六七八九十\d]+空[::]/g, '').trim(); if (sub.querySelector('.marking_cuo')) hasWrong = true; tempAnswers.push(t); } }); if (tempAnswers.length > 0) { finalAns = hasWrong ? "暂未找到正确答案" : tempAnswers.join("; "); found = true; } } else { const myNode = newBx.querySelector('.myAnswerBx .answerCon, .myAnswer .answerCon, .myAnswer'); const statusSpan = newBx.querySelector('.answerScore .CorrectOrNot span'); if (myNode) { let myText = normalizeText(myNode.innerText).replace(/^我的答案[::]/, '').trim(); if (statusSpan && statusSpan.classList.contains('marking_dui')) finalAns = myText; else if (statusSpan && statusSpan.classList.contains('marking_cuo')) finalAns = "暂未找到正确答案"; else finalAns = "(仅供参考) " + myText; } } } } else { const rightSpan = Array.from(qEl.querySelectorAll('.Py_answer span, .answerCon, .rightAnswer')).find(s => s.textContent.includes('正确答案') || s.textContent.includes('我的答案')); if(rightSpan) finalAns = rightSpan.textContent.replace(/正确答案[::]|我的答案[::]/,'').trim(); } if (finalAns === '√' || finalAns === 'true' || finalAns === '对') finalAns = '对'; if (finalAns === '×' || finalAns === 'false' || finalAns === '错') finalAns = '错'; qOutput += "答案:" + finalAns + "\n"; const analysis = qEl.querySelector('.answerKeyBx .answerCon, .Py_addpy .pingyu'); if(analysis) qOutput += "解析:" + getCleanTextFromElement(analysis) + "\n"; } catch (e) { console.error("Error parsing ZyBottom Q:", e); } return { pureQText, rawText: qOutput, answer: finalAns, type: typeName }; } function processFanyaQuestion(qEl, typeName) { let qOutput = ""; let pureQText = "未知题目"; let finalAns = "未能提取答案"; try { const nameEl = qEl.querySelector('h3.mark_name, .mark_title, .question_title, h3'); let qText = "题干未能提取"; if (nameEl) { const contentEl = nameEl.querySelector('.qtContent, .question_text') || nameEl; qText = normalizeQuestionText(getCleanTextFromElement(contentEl)); } pureQText = qText.replace(/^\d+[\.、]\s*/, ''); qOutput += qText + "\n"; qEl.querySelectorAll('.options li, .stem_answer li, .mark_letter li').forEach(opt => qOutput += normalizeText(opt.textContent) + "\n"); let answerBlock = qEl.querySelector('.mark_answer'); if (answerBlock) { const rightAnsEl = answerBlock.querySelector('.rightAnswerContent'); if (rightAnsEl) finalAns = normalizeText(rightAnsEl.textContent); else { const correctMatch = normalizeText(answerBlock.textContent).match(/正确答案[::]\s*([A-Za-z0-9\u4e00-\u9fa5]+)/); if (correctMatch) finalAns = correctMatch[1]; } if (!finalAns || finalAns === "未能提取答案") { const myAnsEl = answerBlock.querySelector('.stuAnswerContent') || answerBlock.querySelector('.mark_fill dd') || answerBlock.querySelector('.mark_fill'); if (myAnsEl) { let myText = getCleanTextFromElement(myAnsEl).replace(/^我的答案[::]/, '').trim(); if (qEl.querySelector('.marking_dui') || answerBlock.querySelector('.marking_dui') || answerBlock.querySelector('.icon_ok')) finalAns = myText; else if (qEl.querySelector('.marking_cuo') || answerBlock.querySelector('.marking_cuo')) finalAns = "暂未找到正确答案"; else finalAns = "(我的答案) " + myText; } } } qOutput += "答案:" + finalAns + "\n"; } catch (e) { console.error("Error parsing Fanya Q:", e); } return { pureQText, rawText: qOutput, answer: finalAns, type: typeName }; } function detectTypeName(text) { if (text.includes("单选题")) return "单选题"; if (text.includes("多选题")) return "多选题"; if (text.includes("判断题")) return "判断题"; if (text.includes("填空题")) return "填空题"; if (text.includes("简答题")) return "简答题"; if (text.includes("资料题") || text.includes("论述题")) return "资料题/论述题"; return text || "未知题型"; } // ========================================================= // 4. 页面原生按钮注入引擎 // ========================================================= function isWorkListPage() { const path = window.location.pathname; return document.querySelector('.bottomList') !== null || document.querySelector('.ulDiv') !== null || // 兼容旧版作业与考试UI path.includes('exam-list') || path.includes('getAllWork') || path.includes('/exam/test'); } function isDetailPage() { return document.querySelector('#ZyBottom') || document.querySelector('#fanyaMarking') || document.querySelector('.questionLi'); } function getPageTitle() { let node = document.querySelector('.task-title') || document.querySelector('.mark_title') || document.querySelector('.Cy_TItle1'); return node ? node.textContent.trim() : (document.title.split('-')[0].trim() || "未命名作业"); } function tryAddBatchButton() { if (!document.body) return; if (!isWorkListPage()) return; if (document.getElementById('cx-export-btn')) return; // 兼容新版和老版不同的工具栏容器 const target = document.querySelector('.bnt_group') || document.querySelector('.filter') || document.querySelector('.top-back') || document.querySelector('.CyTop .ul01') || document.querySelector('.CyTop'); if (target) { const btn = document.createElement('button'); btn.id = 'cx-export-btn'; btn.innerHTML = '📥 一键全自动抓取入库'; btn.onclick = startBatchSpider; if (target.tagName.toLowerCase() === 'ul') { const li = document.createElement('li'); li.style.display = 'inline-block'; li.style.float = 'right'; li.style.marginTop = '4px'; li.appendChild(btn); target.appendChild(li); } else { target.insertBefore(btn, target.firstChild); } } } async function startBatchSpider() { const btn = document.getElementById('cx-export-btn'); // 兼容新旧版列表选择器 const listItems = document.querySelectorAll('.bottomList ul li, .ulDiv ul li'); let tasks =[]; let courseId = document.getElementById('courseId')?.value || ''; let classId = document.getElementById('classId')?.value || ''; let cpi = document.getElementById('cpi')?.value || ''; let openc = document.getElementById('openc')?.value || ''; let fetchAll = GM_getValue(FETCH_ALL_KEY, false); listItems.forEach(li => { let titleNode = li.querySelector('.overHidden2') || li.querySelector('.titTxt a') || li.querySelector('a[title]'); let title = titleNode ? (titleNode.getAttribute('title') || titleNode.innerText.trim()) : "未命名作业"; let url = null; let liText = li.innerText || ""; // “预览”用于匹配新版的题库集(本身无完成状态) let isCompleted = liText.includes('已完成') || liText.includes('已批阅') || liText.includes('查看') || liText.includes('重做') || liText.includes('预览') || /得分/.test(liText); if (!fetchAll && !isCompleted) { return; } // 策略1:直接从已知的按钮上获取 href 或 data let aNode = li.querySelector('a.viewUrl, a.viewBtn, a.Btn_blue_1, a.selfTestPreview'); if (aNode) { let tempUrl = aNode.getAttribute('data') || aNode.getAttribute('href'); if (tempUrl && !tempUrl.includes('javascript:')) { url = tempUrl; } else { let oc = aNode.getAttribute('onclick'); if (oc) { let locMatch = oc.match(/location\.href=['"](.*?)['"]/); if (locMatch) url = locMatch[1]; } } } // 策略2:如果标题本身是个带链接的 a 标签 if (!url && titleNode && titleNode.tagName === 'A') { let tempUrl = titleNode.getAttribute('href'); if (tempUrl && !tempUrl.includes('javascript:')) { url = tempUrl; } } // 策略3:强力提取 onclick 代码中的隐藏参数 (兼容所有旧版和题库版) if (!url) { let clickNodes = li.querySelectorAll('[onclick]'); for (let node of clickNodes) { let oc = node.getAttribute('onclick') || ""; let locMatch = oc.match(/location\.href=['"](.*?)['"]/); if (locMatch) { url = locMatch[1]; break; } let viewMatch = oc.match(/viewPaper\('(\d+)'\)/); if (viewMatch) { url = `/exam-ans/exam/test/reVersionPaperMarkContentNew?courseId=${courseId}&classId=${classId}&p=1&id=${viewMatch[1]}&ut=s&cpi=${cpi}&newMooc=true&openc=${openc}&pageSize=2000`; break; } // 抓取 goTest('courseId', examId, paperId, ...) 中的 paperId let goMatch = oc.match(/goTest\(['"][^'"]*['"]\s*,\s*\d+\s*,\s*(\d+)/); if (goMatch && goMatch[1] !== '0') { let paperId = goMatch[1]; url = `/exam-ans/exam/test/reVersionPaperMarkContentNew?courseId=${courseId}&classId=${classId}&p=1&id=${paperId}&ut=s&cpi=${cpi}&newMooc=true&openc=${openc}&pageSize=2000`; break; } } } if (url && !url.includes('javascript:')) { if (url.startsWith('/')) url = window.location.origin + url; tasks.push({ title, url }); } }); if (tasks.length === 0) { return notifyTopWindow(`⚠️ 当前页面没有${fetchAll ? '任何' : '已完成的'}试卷/作业可抓取!(若要抓自测题,请开启设置中功能)`, true); } btn.disabled = true; notifyTopWindow(`🚀 启动爬虫!将静默爬取 ${tasks.length} 个作业及自动翻页,可能需要稍等片刻...`); let totalAdded = 0; for (let i = 0; i < tasks.length; i++) { const task = tasks[i]; btn.innerHTML = `⏳ 爬取中 (${i + 1}/${tasks.length})`; try { let pageNum = 1; let hasMore = true; let currentUrl = task.url; if (!currentUrl.includes('pageSize')) { currentUrl += (currentUrl.includes('?') ? '&' : '?') + 'pageSize=2000'; } while(hasMore && pageNum <= 30) { let fetchUrl = currentUrl; if (pageNum > 1) { fetchUrl += `&pageNum=${pageNum}&page=${pageNum}`; } const html = await new Promise((res, rej) => { GM_xmlhttpRequest({ method: "GET", url: fetchUrl, onload: (r) => r.status===200 ? res(r.responseText) : rej(r.status), onerror: rej }); }); const doc = new DOMParser().parseFromString(html, 'text/html'); const qs = coreExtract(doc); if (qs && qs.length > 0) { totalAdded += addTaskToDB(task.title, qs); requestTopRender(); } else { hasMore = false; break; } let hasNext = false; doc.querySelectorAll('a').forEach(a => { let t = a.textContent.trim(); if (t === '下一页' || t === '加载更多' || t.includes('下一页')) hasNext = true; }); if (doc.querySelector('.nextPage, .page-next, .next')) hasNext = true; if (hasNext) { pageNum++; await new Promise(r => setTimeout(r, 600)); } else { hasMore = false; } } } catch (e) { console.error("Spider error:", e); } await new Promise(r => setTimeout(r, 600)); } notifyTopWindow(`✅ 批量爬取圆满完成!本次题库净增长 ${totalAdded} 题。`); btn.innerHTML = '📥 一键全自动抓取入库'; btn.disabled = false; requestTopRender(); } function createSinglePageExtractButton() { if (!document.body) return; if (isWorkListPage()) return; if (!isDetailPage()) return; if (document.getElementById('extractQuizButtonGM_merged')) return; const btn = document.createElement('button'); btn.id = 'extractQuizButtonGM_merged'; btn.innerHTML = '⚡ 提取本页入库'; btn.onclick = function() { this.innerHTML = '⏳ 正在提取...'; this.disabled = true; setTimeout(() => { const qs = coreExtract(document); if (qs && qs.length > 0) { let addedCount = addTaskToDB(getPageTitle(), qs); notifyTopWindow(`✅ 成功抓取 ${qs.length} 题,新收录/更新 ${addedCount} 题!`); requestTopRender(); } else { notifyTopWindow('未能找到题目容器', true); } setTimeout(() => { this.innerHTML = '⚡ 提取本页入库'; this.disabled = false; }, 1000); }, 300); }; document.body.appendChild(btn); } // ========================================================= // 5. 最外层主面板 UI 与事件 // ========================================================= function showToastTopWindow(message, isError = false) { let el = document.getElementById('cx-top-notification'); if (!el) { el = document.createElement('div'); el.id = 'cx-top-notification'; document.body.appendChild(el); } el.textContent = message; el.style.backgroundColor = isError ? '#dc3545' : '#28a745'; el.style.display = 'block'; el.style.opacity = '1'; if(el.hideTimer) clearTimeout(el.hideTimer); el.hideTimer = setTimeout(() => { el.style.opacity = '0'; setTimeout(() => { if (el.style.opacity === '0') el.style.display='none'; }, 300); }, 3000); } function createTopPanelUI() { if (!isTopWindow || !document.body) return; if (document.getElementById('cx-qb-float-btn')) return; const floatBtn = document.createElement('button'); floatBtn.id = 'cx-qb-float-btn'; floatBtn.innerHTML = '📚 题库管理'; floatBtn.onclick = () => { document.getElementById('cx-qb-main-panel').style.display = 'flex'; floatBtn.style.display = 'none'; switchViewTop('list'); }; document.body.appendChild(floatBtn); const panel = document.createElement('div'); panel.id = 'cx-qb-main-panel'; panel.innerHTML = `

📚 个人题库工作台

作业名称

系统设置

🤖 自动提取作业

打开作业或测验页面时,自动静默提取题目

📥 抓取所有题目(含未完成)

开启后一键抓取将自动进入所有考试!默认关闭。

🛡️ 解除网页反调试

绕过无限debugger拦截 (需刷新页面生效)

`; document.body.appendChild(panel); bindEventsTop(); } function switchViewTop(viewName) { document.getElementById('cx-view-list').style.display = 'none'; document.getElementById('cx-view-detail').style.display = 'none'; document.getElementById('cx-view-settings').style.display = 'none'; document.getElementById('cx-qb-footer').style.display = (viewName === 'list') ? 'flex' : 'none'; if (viewName === 'list') { document.getElementById('cx-view-list').style.display = 'block'; renderListTop(); } else if (viewName === 'detail') { document.getElementById('cx-view-detail').style.display = 'block'; } else if (viewName === 'settings') { document.getElementById('cx-view-settings').style.display = 'block'; document.getElementById('cx-toggle-auto').checked = GM_getValue(AUTO_EXTRACT_KEY, false); document.getElementById('cx-toggle-fetchall').checked = GM_getValue(FETCH_ALL_KEY, false); document.getElementById('cx-toggle-antidebug').checked = GM_getValue(ANTI_DEBUG_KEY, false); } } function bindEventsTop() { const header = document.getElementById('cx-qb-header'); const panel = document.getElementById('cx-qb-main-panel'); let isDragging = false, offset =[0,0]; header.addEventListener('mousedown', (e) => { if (e.target.closest('.cx-qb-controls')) return; isDragging = true; offset =[panel.offsetLeft - e.clientX, panel.offsetTop - e.clientY]; document.addEventListener('mousemove', dragPanel); document.addEventListener('mouseup', stopDragPanel); }); function dragPanel(e) { if (!isDragging) return; panel.style.left = (e.clientX + offset[0]) + 'px'; panel.style.top = (e.clientY + offset[1]) + 'px'; panel.style.right = 'auto'; } function stopDragPanel() { isDragging = false; document.removeEventListener('mousemove', dragPanel); document.removeEventListener('mouseup', stopDragPanel); } document.getElementById('cx-btn-nav-min').onclick = () => { panel.style.display = 'none'; document.getElementById('cx-qb-float-btn').style.display = 'flex'; }; document.getElementById('cx-btn-nav-settings').onclick = () => switchViewTop('settings'); document.getElementById('cx-btn-back-list').onclick = () => switchViewTop('list'); document.getElementById('cx-btn-back-list2').onclick = () => switchViewTop('list'); document.getElementById('cx-toggle-auto').addEventListener('change', (e) => { GM_setValue(AUTO_EXTRACT_KEY, e.target.checked); showToastTopWindow(`自动提取已 ${e.target.checked ? '开启' : '关闭'}`); }); document.getElementById('cx-toggle-fetchall').addEventListener('change', (e) => { GM_setValue(FETCH_ALL_KEY, e.target.checked); if (e.target.checked) { showToastTopWindow('⚠️ 警告:已允许抓取所有!将强行开启所有未交作业和测验!', true); } else { showToastTopWindow('已关闭抓取所有,后续将严格过滤,仅抓取已完成(有得分/批阅)的作业。'); } }); document.getElementById('cx-toggle-antidebug').addEventListener('change', (e) => { GM_setValue(ANTI_DEBUG_KEY, e.target.checked); showToastTopWindow('🔄 反调试设置已保存,请手动刷新页面生效!', true); }); document.getElementById('cx-btn-g-txt').onclick = () => exportGlobalTop('txt'); document.getElementById('cx-btn-g-json').onclick = () => exportGlobalTop('json'); const clearBtn = document.getElementById('cx-btn-g-clear'); clearBtn.onclick = () => { if (clearBtn.dataset.confirm === '1') { saveDB({ version: "2.0", tasks: {} }); renderListTop(); showToastTopWindow("✅ 题库已成功清空"); clearBtn.dataset.confirm = '0'; clearBtn.textContent = '清空'; } else { clearBtn.dataset.confirm = '1'; clearBtn.textContent = '确定清空?'; setTimeout(() => { clearBtn.dataset.confirm = '0'; clearBtn.textContent = '清空'; }, 3000); } }; document.getElementById('cx-btn-g-import').onclick = importJSONTop; } function renderListTop() { const container = document.getElementById('cx-view-list'); if (!container) return; const db = getDB(); const tasks = db.tasks; container.innerHTML = ''; const keys = Object.keys(tasks); if (keys.length === 0) { container.innerHTML = '
📦 题库空空如也~

页面中会出现原生的【提取】按钮,
点击后数据会自动同步到此处。
'; return; } keys.reverse().forEach(title => { const list = tasks[title]; const item = document.createElement('div'); item.className = 'cx-qb-task-item'; item.innerHTML = `

${title}

${list.length} 题
`; container.appendChild(item); }); container.querySelectorAll('.btn-view').forEach(btn => btn.onclick = (e) => viewDetailTop(e.target.dataset.title)); container.querySelectorAll('.btn-exp-txt').forEach(btn => btn.onclick = (e) => exportSingleTop(e.target.dataset.title, 'txt')); container.querySelectorAll('.btn-exp-json').forEach(btn => btn.onclick = (e) => exportSingleTop(e.target.dataset.title, 'json')); container.querySelectorAll('.btn-del').forEach(btn => { btn.onclick = (e) => { const title = e.target.dataset.title; if (btn.dataset.confirm === '1') { deleteTaskFromDB(title); renderListTop(); showToastTopWindow(`已删除: ${title}`); } else { btn.dataset.confirm = '1'; let originalText = btn.textContent; btn.textContent = '确定?'; setTimeout(() => { if(btn) { btn.dataset.confirm = '0'; btn.textContent = originalText; } }, 3000); } }; }); } function viewDetailTop(title) { const db = getDB(); const qs = db.tasks[title]; if (!qs) return; document.getElementById('cx-detail-title').textContent = title; const body = document.getElementById('cx-detail-body'); let html = ''; qs.forEach((q, idx) => { let rawStr = q.rawText.replace(/^\d+[\.、]\s*/, '').replace(/\n/g, '
'); html += `
${idx + 1}.[${q.type}]
${rawStr}
`; }); body.innerHTML = html; switchViewTop('detail'); } // --- 导出 / 导入 逻辑 --- function formatQuestionsToTXT(title, questionsArray) { if (questionsArray.length === 0) return `# ${title}\n\n[提示:暂无题目数据]\n`; let groups = {}; questionsArray.forEach(q => { if (!groups[q.type]) groups[q.type] = []; groups[q.type].push(q); }); let output = `# ${title}\n\n`; for (let type in groups) { output += `## ${type}\n\n`; groups[type].forEach((q, idx) => { let body = q.rawText.replace(/^\d+[\.、]\s*/, ''); output += `${idx + 1}. ${body}\n\n`; }); output += `\n`; } return output; } function downloadFile(content, filename, type='text/plain') { const blob = new Blob([content], { type: type + ';charset=utf-8' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // 单个任务导出逻辑 (无需改变,因单个任务按题型分就是本身数据) function exportSingleTop(title, type) { const db = getDB(); const qs = db.tasks[title]; if (!qs) return; if (type === 'txt') downloadFile(formatQuestionsToTXT(title, qs), `题库_${title}.txt`); else downloadFile(JSON.stringify({ version: "2.0", tasks: { [title]: qs } }, null, 2), `题库_${title}.json`, 'application/json'); } // 全局汇总导出逻辑 (双TXT模式) function exportGlobalTop(type) { const db = getDB(); const allTasks = Object.keys(db.tasks); if (allTasks.length === 0) return showToastTopWindow("题库是空的!", true); if (type === 'json') { downloadFile(JSON.stringify(db, null, 2), `超星完整题库备份_${new Date().toISOString().slice(0,10)}.json`, 'application/json'); } else { // 模式 1: 按作业集分类 let txtByTask = "========== 超星题库汇总 (按作业集分类) ==========\n\n"; allTasks.forEach(title => { txtByTask += formatQuestionsToTXT(title, db.tasks[title]) + "\n"; }); downloadFile(txtByTask, `题库汇总_按作业集_${new Date().toISOString().slice(0,10)}.txt`); // 模式 2: 全局按题型合并并去重 let allQs =[]; allTasks.forEach(t => allQs = allQs.concat(db.tasks[t])); let mergedQs = mergeQuestions([], allQs); let txtByType = formatQuestionsToTXT("超星题库汇总 (全局按题型分类)", mergedQs); showToastTopWindow("正在为您导出 2 份 TXT,请在弹窗中【允许浏览器下载多个文件】!"); // 稍加延迟,保证浏览器不会拦截第二个下载 setTimeout(() => { downloadFile(txtByType, `题库汇总_按题型_${new Date().toISOString().slice(0,10)}.txt`); }, 800); } } function importJSONTop() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json'; input.onchange = e => { const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = ev => { try { const imported = JSON.parse(ev.target.result); if (!imported.tasks) throw new Error("JSON 格式不兼容"); const db = getDB(); let newTasks = 0, newQs = 0; for (let title in imported.tasks) { if (!db.tasks[title]) db.tasks[title] =[]; let oldLen = db.tasks[title].length; db.tasks[title] = mergeQuestions(db.tasks[title], imported.tasks[title]); if (!oldLen) newTasks++; newQs += (db.tasks[title].length - oldLen); } saveDB(db); renderListTop(); showToastTopWindow(`🎉 导入成功!新增/更新了 ${newTasks} 个作业的 ${newQs} 道题。`); } catch(err) { showToastTopWindow("导入失败,文件损坏或格式不正确!", true); } }; reader.readAsText(file); }; input.click(); } // ========================================================= // 6. 初始化与轮询通信机制 // ========================================================= setInterval(() => { tryAddBatchButton(); createSinglePageExtractButton(); }, 1500); setInterval(() => { if (!GM_getValue(AUTO_EXTRACT_KEY, false)) return; if (!isDetailPage()) return; if (window._cx_auto_extracted) return; let container = document.querySelector('#ZyBottom') || document.querySelector('#fanyaMarking') || document.querySelector('.questionLi')?.parentElement; if (container) { window._cx_auto_extracted = true; setTimeout(() => { let qs = coreExtract(document); if (qs.length > 0) { let added = addTaskToDB(getPageTitle(), qs); // 【修复核心1】 仅当实际增加了新的题目时,才向页面推送弹窗,避免多次触发 if (added > 0) { notifyTopWindow(`🤖 自动提取完毕!新收录/更新 ${added} 题`); } else { console.log("[超星提取器] 题目已在库中,无需更新。"); } requestTopRender(); } }, 1000); } }, 2000); if (isTopWindow) { let initUIInterval = setInterval(() => { if (document.body) { clearInterval(initUIInterval); createTopPanelUI(); } }, 100); let lastNotifyToken = 0; let lastRenderToken = 0; setInterval(() => { let notifyMsg = GM_getValue(COM_NOTIFY, { msg: '', t: 0 }); if (notifyMsg.t !== lastNotifyToken) { lastNotifyToken = notifyMsg.t; showToastTopWindow(notifyMsg.msg, notifyMsg.isError); } let renderMsg = GM_getValue(COM_RENDER, 0); if (renderMsg !== lastRenderToken) { lastRenderToken = renderMsg; const panel = document.getElementById('cx-qb-main-panel'); if (panel && panel.style.display === 'flex') { renderListTop(); } } }, 300); } })();