// ==UserScript== // @name 西电MOOC通用自动答题(v14 数据库优先+可视化版) // @namespace http://tampermonkey.net/ // @version 14.0 // @description 数据库最高优先级,支持搜索、预览匹配、逐题结果展示、颜色区分 // @match *://mooc1.xidian.edu.cn/* // @match *://mooc2-ans.xidian.edu.cn/* // @match *://*.xidian.edu.cn/* // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript== (function () { 'use strict'; /* ===================== 配置区 ===================== */ const CONFIG = { questionBlockSelector: '.questionLi, .TiMu, .exam-question, [class*="question-block"]', stemSelector: '.mark_name, .qt-title, h3.u-tit, h3, .question-stem', optionSelector: '.answerBg, .position, .qt-item, .choice, [class*="option"], ul li, .option-item', optionLabelSelector: 'input, .label, [class*="lbl"], .option-label', submitSelector: '.btn-blue, [class*="submit"], a.btn, .moco-btn, .submit-btn, .completeBtn', stepDelay: 400, verbose: true, }; const log = (...a) => CONFIG.verbose && console.log('%c[AutoAns]', 'color:#1F3A2E;font-weight:bold', ...a); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); /* ===================== 核心逻辑 ===================== */ // 全局数据源 let dbQuestionList = []; // 数据库题库列表 let finalAnswerMap = {}; // 最终答题用的题号->答案映射 let manualTemplateMap = {}; // 手动粘贴的模板 // 题干归一化 function normalizeKey(s) { return (s || '') .replace(/\s+/g, '') .replace(/(/g, '(').replace(/)/g, ')') .replace(/\(\s*\)/g, '') .replace(/[,。、;:!?.,;:!?]/g, '') .toLowerCase() .trim(); } // Bigram 相似度计算 function getSimilarity(str1, str2) { if (str1 === str2) return 1; const len1 = str1.length, len2 = str2.length; if (len1 < 2 || len2 < 2) return 0; const getBigrams = (str) => { const set = new Map(); for (let i = 0; i < str.length - 1; i++) { const bigram = str.substring(i, i + 2); set.set(bigram, (set.get(bigram) || 0) + 1); } return set; }; const bigrams1 = getBigrams(str1), bigrams2 = getBigrams(str2); let intersection = 0; bigrams1.forEach((count, bigram) => { if (bigrams2.has(bigram)) intersection += Math.min(count, bigrams2.get(bigram)); }); return (2.0 * intersection) / (len1 - 1 + len2 - 1); } // 解析 JSON 题库 function parseQuestionBank(rawJson) { let data = typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson; let questions = []; if (Array.isArray(data)) questions = data; else if (data.questions && Array.isArray(data.questions)) questions = data.questions; else if (!Array.isArray(data) && typeof data === 'object') questions = Object.entries(data).map(([stem, answer]) => ({ stem, correctAnswer: answer })); else throw new Error("无法识别的JSON格式"); return questions.map(q => { const stem = q.stem || q.q || q.question || ''; let answer = q.correctAnswer || q.a || q.answer || q.ans || ''; if (Array.isArray(answer)) answer = answer.join(''); if (typeof answer === 'boolean') answer = answer ? 'A' : 'B'; return { stem, correctAnswer: String(answer).toUpperCase().replace(/[^A-Z]/g, '') }; }).filter(q => q.stem && q.correctAnswer); } // 解析答案模板 function parseAnswerTemplate(text) { const map = {}; const lines = text.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('###')) continue; const rangeMatch = trimmed.match(/(\d+)\s*[-‑]\s*(\d+)\s*[::]\s*(.+)/); if (rangeMatch) { const start = parseInt(rangeMatch[1]), end = parseInt(rangeMatch[2]); const answers = rangeMatch[3].trim().split(/\s+/); for (let i = 0; i <= end - start; i++) { if (answers[i]) map[start + i] = answers[i].toUpperCase().replace(/[^A-Z]/g, ''); } continue; } const singleMatch = trimmed.match(/(\d+)\s*[.::\s]\s*([A-Z]+)/g); if (singleMatch) { for (const item of singleMatch) { const m = item.match(/(\d+)\s*[.::\s]\s*([A-Z]+)/); if (m) map[parseInt(m[1])] = m[2].toUpperCase(); } } } return map; } // 获取选项字母 function getOptionLabel(opt) { const labelEl = opt.querySelector(CONFIG.optionLabelSelector); if (labelEl) { const v = (labelEl.value || labelEl.innerText || '').trim(); if (v) return v.charAt(0).toUpperCase(); } const m = (opt.innerText || '').trim().match(/^([A-Z])/); return m ? m[1] : ''; } // 核心:重建最终答案池(数据库优先,模板兜底) function rebuildFinalAnswerMap() { finalAnswerMap = {}; // 1. 先写入手动模板 Object.assign(finalAnswerMap, manualTemplateMap); // 2. 再用数据库按顺序覆盖(数据库优先级最高) dbQuestionList.forEach((q, idx) => { finalAnswerMap[idx + 1] = q.correctAnswer; }); } /* ===================== 可视化与答题逻辑 ===================== */ // 执行自动答题(isPreview 为 true 时只预览不点击) async function runAutoAnswer(isPreview = false) { const blocks = document.querySelectorAll(CONFIG.questionBlockSelector); const resultPanel = document.getElementById('__aa-result-list'); const statusEl = document.getElementById('__aa-status'); if (blocks.length === 0) { statusEl.textContent = '⚠ 未找到题目块'; statusEl.style.color = '#A8442A'; return; } resultPanel.innerHTML = ''; // 清空旧结果 let stats = { ok: 0, miss: 0, err: 0 }; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; const stemEl = block.querySelector(CONFIG.stemSelector); const stemText = stemEl ? stemEl.innerText.trim().substring(0, 30) : `第${i+1}题`; const correct = finalAnswerMap[i + 1]; // 清除之前的边框样式 block.style.borderLeft = ''; let resHtml = ''; if (!correct) { // 未收录 stats.miss++; block.style.borderLeft = '4px solid #FF9800'; resHtml = `
未收录 ${i+1}. ${stemText}...
`; } else { const opts = block.querySelectorAll(CONFIG.optionSelector); let clicked = 0; if (!isPreview) { // 实际点击逻辑 for (const letter of correct) { for (const opt of opts) { if (getOptionLabel(opt) === letter) { opt.click(); clicked++; await sleep(CONFIG.stepDelay); break; } } } } else { // 预览模式:假设都能点上 clicked = correct.length; } if (clicked > 0) { stats.ok++; block.style.borderLeft = '4px solid #4CAF50'; resHtml = `
${isPreview ? '已匹配' : '已选'} ${i+1}. ${stemText}... ➔ ${correct}
`; } else { stats.err++; block.style.borderLeft = '4px solid #F44336'; resHtml = `
选项缺失 ${i+1}. ${stemText}... (答案:${correct})
`; } } resultPanel.innerHTML += resHtml; } // 更新底部统计 statusEl.innerHTML = `
✔ 已选 ${stats.ok} 题 | ⚠ 未收录 ${stats.miss} 题 | ✖ 异常 ${stats.err} 题
共扫描 ${blocks.length} 个题目块
`; if (!isPreview) { const submit = document.querySelector(CONFIG.submitSelector); if (submit) { submit.style.boxShadow = '0 0 0 3px #A8442A'; submit.style.border = '2px solid #A8442A'; } } } /* ===================== 悬浮窗 UI ===================== */ function createUI() { const container = document.createElement('div'); container.id = '__autoans-ui'; container.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:99999;font-family:sans-serif;'; container.innerHTML = ` `; document.body.appendChild(container); const panel = container.querySelector('#__aa-panel'); const toggleBtn = container.querySelector('#__aa-toggle'); const fileInput = container.querySelector('#__aa-file'); const templateArea = container.querySelector('#__aa-template'); const statusEl = container.querySelector('#__aa-status'); const searchInput = container.querySelector('#__aa-search'); const searchResult = container.querySelector('#__aa-search-result'); toggleBtn.onclick = () => panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; // 导入数据库 fileInput.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (evt) => { try { dbQuestionList = parseQuestionBank(evt.target.result); rebuildFinalAnswerMap(); statusEl.textContent = `✓ 数据库加载成功:${dbQuestionList.length} 题`; statusEl.style.color = '#1F3A2E'; } catch(err) { statusEl.textContent = `✗ 数据库解析失败:${err.message}`; statusEl.style.color = '#A8442A'; } }; reader.readAsText(file); }; // 解析补充模板 container.querySelector('#__aa-parse').onclick = () => { try { const text = templateArea.value.trim(); if (!text) throw new Error("模板内容为空"); manualTemplateMap = parseAnswerTemplate(text); rebuildFinalAnswerMap(); const count = Object.keys(manualTemplateMap).length; statusEl.textContent = `✓ 补充模板已合并:${count} 题,当前共 ${Object.keys(finalAnswerMap).length} 题`; statusEl.style.color = '#1F3A2E'; } catch(err) { statusEl.textContent = `✗ 模板解析失败:${err.message}`; statusEl.style.color = '#A8442A'; } }; // 预览匹配 container.querySelector('#__aa-preview').onclick = () => runAutoAnswer(true); // 执行答题 container.querySelector('#__aa-run').onclick = () => { if (Object.keys(finalAnswerMap).length === 0) { statusEl.textContent = '⚠ 答案池为空,请先导入数据库或模板'; statusEl.style.color = '#A8442A'; return; } runAutoAnswer(false); }; // 搜索功能 searchInput.oninput = () => { const keyword = normalizeKey(searchInput.value); if (!keyword) { searchResult.innerHTML = ''; return; } let html = ''; const exact = dbQuestionList.find(q => normalizeKey(q.stem).includes(keyword)); if (exact) { html += `
答案:${exact.correctAnswer}
${exact.stem.substring(0, 50)}...
`; } const fuzzy = dbQuestionList.filter(q => { const sim = getSimilarity(keyword, normalizeKey(q.stem)); return sim > 0.4 && sim < 1; }).slice(0, 3); fuzzy.forEach(q => { html += `
答案:${q.correctAnswer}
${q.stem.substring(0, 50)}...
`; }); if (!html) html = '
未找到相关题目
'; searchResult.innerHTML = html; }; } createUI(); log('西电MOOC自动答题脚本 v14 (终极可视化版) 已加载'); })();