// ==UserScript== // @name 超星学习通自动答题 // @namespace http://tampermonkey.net/ // @version 2.0.2 // @description 调用 DeepSeek API 自动作答,支持单选、多选、判断、填空、简答,带自动翻页、模型选择、智能暂停、温度调节与 token 统计 // @author dmhnb6 // @match *://mooc1.chaoxing.com/mooc-ans/mooc2/work/dowork* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect api.deepseek.com // ==/UserScript== (function() { 'use strict'; // =========================== // 用户默认配置 // =========================== let API_KEY = GM_getValue('deepseek_api_key', ''); let MODEL = GM_getValue('model', 'deepseek-v4-flash'); let ENABLE_THINKING = GM_getValue('enable_thinking', false); let TEMPERATURE = GM_getValue('temperature', 0.1); // 新增温度参数 const DELAY = 200; const JUMP_DELAY = 200; // 全局状态 let isPaused = false; let isRunning = false; let solvingController = null; let currentSheetItems = []; let currentIdx = 0; // UI 元素 let progressBar, progressText, statusLabel; let startBtn, pauseBtn, resumeCurrentBtn; let modelSelect, thinkingCheckbox, temperatureInput; const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // 获取可用模型列表 async function fetchModels(apiKey = API_KEY) { return new Promise((resolve) => { if (!apiKey || apiKey.trim() === '') { resolve(['deepseek-v4-flash', 'deepseek-v4-pro']); return; } GM_xmlhttpRequest({ method: 'GET', url: 'https://api.deepseek.com/models', headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${apiKey}` }, timeout: 8000, onload: function(res) { try { const data = JSON.parse(res.responseText); const models = data.data?.map(m => m.id)?.filter(id => id) || []; resolve(models.length ? models : ['deepseek-v4-flash', 'deepseek-v4-pro']); } catch { resolve(['deepseek-v4-flash', 'deepseek-v4-pro']); } }, onerror: () => resolve(['deepseek-v4-flash', 'deepseek-v4-pro']), ontimeout: () => resolve(['deepseek-v4-flash', 'deepseek-v4-pro']) }); }); } function createUI() { const panel = document.createElement('div'); panel.style = ` position: fixed; top: 10px; left: 50%; transform: translateX(-50%); z-index: 10000; width: 540px; background: #fff; padding: 15px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); font-family: sans-serif; border: 1px solid #ddd; font-size: 13px; `; panel.innerHTML = `
🔑 Key:
✍️ 答题助手 (DeepSeek) 待命中
模型:
准备就绪
`; document.body.appendChild(panel); progressBar = document.getElementById('auto-progress-bar'); progressText = document.getElementById('auto-progress-text'); statusLabel = document.getElementById('auto-status-label'); startBtn = document.getElementById('auto-start-btn'); pauseBtn = document.getElementById('auto-pause-btn'); resumeCurrentBtn = document.getElementById('auto-resume-current-btn'); modelSelect = document.getElementById('auto-model-select'); thinkingCheckbox = document.getElementById('auto-thinking-checkbox'); temperatureInput = document.getElementById('auto-temperature-input'); fetchModels().then(models => { modelSelect.innerHTML = models.map(m => ``).join(''); }); modelSelect.addEventListener('change', () => { MODEL = modelSelect.value; GM_setValue('model', MODEL); }); thinkingCheckbox.checked = ENABLE_THINKING; thinkingCheckbox.addEventListener('change', () => { ENABLE_THINKING = thinkingCheckbox.checked; GM_setValue('enable_thinking', ENABLE_THINKING); }); // temperatureInput.addEventListener('change', () => { // TEMPERATURE = parseFloat(temperatureInput.value) || 0.6; // GM_setValue('temperature', TEMPERATURE); // }); temperatureInput.addEventListener('change', () => { let val = parseFloat(temperatureInput.value); if (isNaN(val) || val < 0) val = 0; else if (val > 2) val = 2; temperatureInput.value = val; TEMPERATURE = val; GM_setValue('temperature', val); alert(`⚠️ 温度值已自动修正为 ${val}(允许范围 0~2)`); }); startBtn.onclick = () => { if (!API_KEY || API_KEY.trim() === '') { alert('❌ 请先输入并保存 DeepSeek API Key!'); return; } if (typeof TEMPERATURE !== 'number' || isNaN(TEMPERATURE) || TEMPERATURE < 0 || TEMPERATURE > 2) { TEMPERATURE = 0.1; temperatureInput.value = 0.1; GM_setValue('temperature', 0.1); alert('⚠️ 温度值已自动修正为 0.1(允许范围 0~2)'); } const isRestart = (startBtn.innerText === '重新开始'); if (isRestart) { currentIdx = 0; isPaused = false; statusLabel.innerText = "正在重新开始..."; } if (!isRunning || isRestart) { if (solvingController) solvingController.abort(); isRunning = true; startBtn.style.display = 'none'; pauseBtn.style.display = 'inline-block'; pauseBtn.innerText = '暂停'; pauseBtn.style.background = '#e74c3c'; resumeCurrentBtn.style.display = 'none'; startSolve(); } }; pauseBtn.onclick = () => { isPaused = !isPaused; if (isPaused) { pauseBtn.innerText = '恢复'; pauseBtn.style.background = '#f1c40f'; statusLabel.innerText = '已暂停'; startBtn.innerText = '重新开始'; startBtn.style.background = '#3498db'; startBtn.style.display = 'inline-block'; resumeCurrentBtn.style.display = 'inline-block'; } else { currentIdx = Math.max(0, currentIdx - 1); pauseBtn.innerText = '暂停'; pauseBtn.style.background = '#e74c3c'; statusLabel.innerText = '运行中'; startBtn.style.display = 'none'; resumeCurrentBtn.style.display = 'none'; } }; resumeCurrentBtn.onclick = () => { if (solvingController) solvingController.abort(); isPaused = false; const currentLi = getCurrentLi(); if (currentLi) { const items = document.querySelectorAll('.topicNumber_list li'); currentIdx = Array.from(items).indexOf(currentLi); if (currentIdx < 0) currentIdx = 0; } else { currentIdx = 0; } isRunning = true; startBtn.style.display = 'none'; pauseBtn.style.display = 'inline-block'; pauseBtn.innerText = '暂停'; pauseBtn.style.background = '#e74c3c'; resumeCurrentBtn.style.display = 'none'; statusLabel.innerText = `从第 ${currentIdx+1} 题开始...`; startSolve(); }; updateResumeCurrentVisibility(); const observer = new MutationObserver(() => updateResumeCurrentVisibility()); document.querySelectorAll('.topicNumber_list').forEach(list => observer.observe(list, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }) ); const keyInput = document.getElementById('auto-api-key-input'); const saveKeyBtn = document.getElementById('auto-save-key-btn'); const keyStatus = document.getElementById('auto-key-status'); keyInput.value = API_KEY; saveKeyBtn.addEventListener('click', async () => { const newKey = keyInput.value.trim(); if (!newKey) { keyStatus.textContent = 'Key 不能为空'; keyStatus.style.color = 'red'; return; } GM_setValue('deepseek_api_key', newKey); API_KEY = newKey; keyStatus.textContent = '已保存,正在刷新模型列表...'; keyStatus.style.color = '#27ae60'; const models = await fetchModels(newKey); modelSelect.innerHTML = models.map(m => ``).join(''); keyStatus.textContent = '✅ 已保存'; setTimeout(() => { if (keyStatus.textContent === '✅ 已保存') keyStatus.textContent = ''; }, 2000); }); } function getCurrentLi() { return document.querySelector('.topicNumber_list li.current.active') || document.querySelector('.topicNumber_list li.current'); } function updateResumeCurrentVisibility() { const currentLi = getCurrentLi(); if (!isRunning && !isPaused) { resumeCurrentBtn.style.display = currentLi ? 'inline-block' : 'none'; } } function startSolve() { if (!API_KEY || API_KEY.trim() === '') { alert('请先输入并保存 DeepSeek API Key!'); return; } const sheetItems = document.querySelectorAll('.topicNumber_list li'); if (!sheetItems.length) { alert('未找到答题卡题目'); return; } currentSheetItems = Array.from(sheetItems); solvingController = new AbortController(); solveAllQuestions(solvingController.signal); } function waitForQuestionElement(qId, timeout = JUMP_DELAY) { const startTime = Date.now(); return new Promise((resolve) => { const check = () => { const el = document.querySelector(`.questionLi[data="${qId}"]`); if (el) resolve(el); else if (Date.now() - startTime > timeout) resolve(null); else requestAnimationFrame(check); }; check(); }); } async function solveAllQuestions(signal) { const total = currentSheetItems.length; let completed = 0, errors = 0; // Token 统计 let totalTokens = { prompt: 0, completion: 0, total: 0 }; for (let i = currentIdx; i < total; i++) { if (signal.aborted) return; while (isPaused && !signal.aborted) await sleep(300); if (signal.aborted) return; const item = currentSheetItems[i]; const qId = item.getAttribute('data'); if (!qId) { errors++; continue; } statusLabel.innerText = `跳转至第 ${i+1} 题...`; item.click(); const questionEl = await waitForQuestionElement(qId); if (signal.aborted) return; if (!questionEl) { errors++; continue; } const type = questionEl.getAttribute('typeName') || '单选题'; const percent = Math.floor(((i + 1) / total) * 100); progressBar.style.width = `${percent}%`; progressText.innerText = `${i + 1} / ${total} (${percent}%)`; statusLabel.innerText = `处理第 ${i+1} 题 (${type})`; const titleEl = questionEl.querySelector('h3.mark_name'); if (!titleEl) { errors++; continue; } const titleClone = titleEl.cloneNode(true); const typeSpan = titleClone.querySelector('.colorShallow'); if (typeSpan) typeSpan.remove(); const questionText = titleClone.textContent.replace(/\s+/g, ' ').trim(); try { const optionsStr = (type !== '判断题' && ['单选题','多选题','判断题'].includes(type)) ? extractOptions(questionEl) : ''; const aiResult = await askAI(type, questionText, optionsStr, signal); if (aiResult !== null && aiResult.answer) { const aiAnswer = aiResult.answer; console.log(`[题目 ${i+1}] 类型: ${type} | 题干: ${questionText.substring(0, 80)}${questionText.length > 80 ? '...' : ''}`); if (optionsStr) console.log(`[选项]: ${optionsStr}`); console.log(`[AI返回]:`, aiAnswer); // 记录 token 用量 if (aiResult.usage) { totalTokens.prompt += aiResult.usage.prompt_tokens || 0; totalTokens.completion += aiResult.usage.completion_tokens || 0; totalTokens.total += aiResult.usage.total_tokens || 0; } if (['单选题', '多选题', '判断题'].includes(type)) { selectOption(questionEl, aiAnswer, type) ? completed++ : errors++; } else { fillTextAnswer(questionEl, aiAnswer, type) ? completed++ : errors++; } } else { errors++; } currentIdx = i + 1; await sleep(DELAY); } catch (e) { if (e.name === 'AbortError') return; errors++; currentIdx = i + 1; } } isRunning = false; progressBar.style.width = '100%'; progressText.innerText = `完成 ${completed}/${total},失败 ${errors} 题`; statusLabel.innerText = "全部作答完成"; pauseBtn.style.display = 'none'; startBtn.style.display = 'inline-block'; startBtn.innerText = '重新开始'; startBtn.style.background = '#2ecc71'; updateResumeCurrentVisibility(); alert(`✅ 自动答题完成!\n成功:${completed} 题\n失败/跳过:${errors} 题\n总共消耗 tokens:${totalTokens.total} (输入: ${totalTokens.prompt}, 输出: ${totalTokens.completion})`); } function askAI(type, question, optionsStr, signal) { let sys = "你是一个考试助手。严格遵循输出格式,禁止任何解释、前缀、后缀、标点、换行或推理过程。答案符合法律法规和社会主义核心价值观。"; let maxTok = 10; if (type === "单选题" || type === "判断题") { sys += "不解释!只返回单个字母(A/B/C/D)或'对'/'错'。"; maxTok = 1; } else if (type === "多选题") { sys += "不解释!只返回连续的正确字母组合(如ABC),无空格无分隔符。"; maxTok = 3; } else if (type === "填空题") { sys += "不解释!多空答案严格用###分隔,无其他字符。"; maxTok = 45; } else if (type === "简答题") { sys += "不解释!提供精炼答案,答案积极,不要包含多余废话。"; maxTok = 90; } const userMessage = `题型:${type}\n题目:${question}${optionsStr ? '\n选项:' + optionsStr : ''}`; const body = { model: MODEL, messages: [ { role: 'system', content: sys }, { role: 'user', content: userMessage } ], temperature: TEMPERATURE, top_p: 0.95, stream: false, }; if (!ENABLE_THINKING) { body.max_tokens = maxTok; body.thinking = {type : 'disabled'}; } return new Promise((resolve) => { const xhr = GM_xmlhttpRequest({ method: 'POST', url: 'https://api.deepseek.com/chat/completions', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` }, data: JSON.stringify(body), timeout: 15000, onload: function(res) { try { const data = JSON.parse(res.responseText); console.log(`[API响应]:`, data); const raw = data.choices?.[0]?.message?.content?.trim() ?? null; const answer = cleanAIResponse(raw, type); const usage = data.usage || {}; resolve({ answer, usage }); } catch { resolve(null); } }, onerror: () => resolve(null), ontimeout: () => resolve(null) }); if (signal) { signal.addEventListener('abort', () => { if (xhr?.abort) xhr.abort(); resolve(null); }); } }); } function cleanAIResponse(text, type) { if (!text) return null; if (type === '单选题') return text.match(/[A-Da-d]/)?.[0]?.toUpperCase() || text; if (type === '判断题') { if (/对|正确|√/i.test(text)) return '对'; if (/错|错误|×/i.test(text)) return '错'; return text; } if (type === '多选题') return text.replace(/[^A-Da-d]/g, '').toUpperCase() || text; return text; } function extractOptions(questionEl) { return Array.from(questionEl.querySelectorAll('.answerBg')).map(div => { const letterSpan = div.querySelector('.num_option, .num_option_dx, [data]'); const letter = letterSpan ? letterSpan.textContent.trim().replace(/[\.、]/g, '') : ''; const answerP = div.querySelector('.answer_p'); let text = answerP ? answerP.textContent.trim() : ''; if (!text && letter) { text = div.textContent .replace(new RegExp(`^\\s*${letter}[\.、]??\\s*`), '') .replace(/\s+/g, ' ') .trim(); } return (letter && text && text.length > 1) ? `${letter}. ${text}` : ''; }).filter(Boolean).join(' | '); } function selectOption(questionEl, aiAnswer, type) { let selected = false; questionEl.querySelectorAll('.answerBg').forEach(div => { const letterSpan = div.querySelector('.num_option, .num_option_dx'); const letter = letterSpan ? letterSpan.textContent.trim().toUpperCase() : ''; const isChecked = div.getAttribute('aria-checked') === 'true'; if ((type === '单选题' || type === '多选题') && aiAnswer.includes(letter) && !isChecked) { div.click(); selected = true; } else if (type === '判断题') { const text = div.textContent.trim(); if ((aiAnswer.includes('对') && text.includes('对')) || (aiAnswer.includes('错') && text.includes('错'))) { if (!isChecked) { div.click(); selected = true; } } } }); return selected; } function fillTextAnswer(questionEl, aiAnswer, type) { if (!aiAnswer) return false; const textareas = questionEl.querySelectorAll('textarea[id^="answer"]'); if (!textareas.length) return false; const answers = type === '填空题' ? aiAnswer.split('###') : [aiAnswer]; textareas.forEach((ta, idx) => { const id = ta.id; if (typeof UE !== 'undefined' && UE.getEditor) { try { const editor = UE.getEditor(id); if (editor?.ready) editor.ready(() => editor.setContent(answers[idx] || answers[0])); } catch { ta.value = answers[idx] || answers[0]; } } else ta.value = answers[idx] || answers[0]; ta.dispatchEvent(new Event('input', { bubbles: true })); }); const qId = questionEl.getAttribute('data'); try { if (typeof window.loadEditorAnswered === 'function') { window.loadEditorAnswered(qId, type === '填空题' ? 2 : 4); } } catch (e) { console.warn('Chaoxing API call failed:', e); } return true; } window.addEventListener('load', () => setTimeout(createUI, 2000)); })();