// ==UserScript== // @name 优学院答题助手 V14.9 // @namespace https://thewinds.me/ // @version 14.9 // @description 首次运行提示配置API | 自定义AI接口 | 双核驱动 | 全自动刷课 | 智能避让 // @author Winds // @license CC-BY-NC-4.0 // @match *://*.ulearning.cn/* // @connect homeworkapi.ulearning.cn // @connect workers.dev // @connect * // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @downloadURL none // ==/UserScript== /* * Author: Winds (https://thewinds.me) * Ref: 墨青 (https://blog.blackcyan.top/) * * 【版权声明】 * 本脚本采用 CC-BY-NC-4.0 协议进行授权。 * 您可以自由使用、修改和分发本脚本,但【严禁用于任何商业用途】。 * 禁止将本脚本打包出售、植入广告或用于其他盈利性行为。 */ (function() { 'use strict'; // ================= 配置区域 ================= // 从油猴存储中读取用户配置的 URL,默认为空 const USER_API_URL = GM_getValue('UL_AI_URL', ''); const CONFIG = { // 动态获取 API 地址 get aiBaseUrl() { return USER_API_URL; }, interval: 2000, selectors: { listContainer: ".table-homework", writeBtn: ".item-operation .button-red-solid", nextPageBtn: ".pagination-wrap .next", questionContainer: ".question-choice, .question-gap-filling, .question-short-answer", titleContext: ".title", question: ".question-title", textInput: ".ul-textarea__inner", choiceItem: ".choice-item", choiceText: ".choice-title", choiceIndex: ".index", choiceClickArea: ".ul-radio, .ul-checkbox", judgeTrueIcon: ".icon-zhengque", judgeFalseIcon: ".icon-cuowu1", checkedSelector: ".is-checked, .is-active, input:checked", startQuizBtn: ".ul-button--primary", bottomBarNum: ".number .answered", submitBtn: ".ul-button--primary", modalConfirmBtn: ".ul-message-box .ul-button--primary", headerBackBtn: ".header-back, .goback, .icon-fanhui", resultPageMarker: ".homework-result-report, .score-panel", itemName: ".item-name", checkboxClass: ".ul-checkbox" } }; let API_ANSWERS = {}; let HAS_FETCHED_API = false; // ================= 首次运行检查 ================= function checkFirstRun() { if (!GM_getValue('UL_HAS_INIT', false)) { const tips = "🎉 欢迎使用优学院答题助手 V14.9!\n\n" + "请输入您的 AI API 地址:\n" + "👉 如果填入:脚本将使用 AI 回答填空题和主观题。\n" + "👉 如果留空:脚本将【自动跳过】所有主观题,只做客观题。\n\n" + "(您之后可以点击面板上的 ⚙️ 按钮随时修改)"; const input = prompt(tips, ""); if (input !== null) { GM_setValue('UL_AI_URL', input.trim()); GM_setValue('UL_HAS_INIT', true); location.reload(); // 刷新以生效 } } } // ================= UI 样式 ================= GM_addStyle(` #ai-panel { position: fixed; top: 20px; right: 20px; width: 320px; height: 420px; background: #fff; box-shadow: 0 4px 20px rgba(0,0,0,0.15); border-radius: 12px; z-index: 99999; font-family: sans-serif; border: 1px solid #ebeef5; display: flex; flex-direction: column; transition: all 0.3s; overflow: hidden; } #ai-header { padding: 12px 15px; background: #2c3e50; color: white; /* 深色 V14.9 */ height: 44px; box-sizing: border-box; font-weight: 600; display: flex; justify-content: space-between; align-items: center; cursor: move; } #ai-content { padding: 15px; overflow-y: auto; flex-grow: 1; display: flex; flex-direction: column; } /* 链接样式 */ .author-link { color: #2c3e50; text-decoration: none; font-weight: bold; transition: color 0.2s; } .author-link:hover { color: #e74c3c; text-decoration: underline; } .ref-link { color: #95a5a6; text-decoration: none; transition: color 0.2s; } .ref-link:hover { color: #333; text-decoration: underline; } .mode-badge { display:inline-block; padding:2px 6px; border-radius:4px; font-size:12px; color:white; margin-bottom:5px; } .badge-api { background: #27ae60; } .badge-ai { background: #2980b9; } .ai-btn { background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.6); color: white; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 5px; } .ai-btn:hover { background: rgba(255,255,255,0.3); } .reasoning { color: #666; font-style: italic; background: #f8f9fa; padding: 8px; margin-bottom: 10px; font-size: 12px; border-left: 3px solid #ddd; } .answer { color: #333; font-weight: 600; white-space: pre-wrap; } .current-q { border: 2px solid #2c3e50 !important; box-shadow: 0 0 10px rgba(44,62,80,0.2); } #ai-panel.minimized { width: 50px !important; height: 50px !important; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 4px 15px rgba(0,0,0,0.3); cursor: pointer; background-color: #2c3e50; } #ai-panel.minimized #ai-content, #ai-panel.minimized #ai-header { opacity: 0; pointer-events: none; } #ai-panel.minimized::after { content: "⚙️"; font-size: 24px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; pointer-events: none; } `); // ================= 状态管理 ================= const isListPage = () => document.querySelector(CONFIG.selectors.listContainer) !== null; const isResultPage = () => location.href.includes("stuReport") || document.querySelector(CONFIG.selectors.resultPageMarker) !== null; const isPotentialQuizPage = () => !isListPage() && !isResultPage() && ( document.querySelectorAll(CONFIG.selectors.questionContainer).length > 0 || document.querySelector(CONFIG.selectors.startQuizBtn) !== null ); let isRunning = false; let isPaused = false; let questionsList = []; let currentIndex = 0; // ================= 核心:API 答案获取 ================= function getIdsFromUrl() { const url = location.href; const ocIdMatch = url.match(/ocId=(\d+)/); const homeworkIdMatch = url.match(/homeworkId=(\d+)/); if (ocIdMatch && homeworkIdMatch) return { ocId: ocIdMatch[1], homeworkId: homeworkIdMatch[1] }; return null; } function getToken() { const match = document.cookie.match(/token=([^;]+)/); return match ? match[1] : localStorage.getItem("token"); } function fetchStandardAnswers(callback) { const ids = getIdsFromUrl(); if (!ids) { if(callback) callback(false); return; } const token = getToken(); if (!token) { if(callback) callback(false); return; } const apiUrl = `https://homeworkapi.ulearning.cn/quiz/homework/stu/questions?homeworkId=${ids.homeworkId}&ocId=${ids.ocId}&showAnswer=true`; const statusEl = document.querySelector('#status-text'); if(statusEl) statusEl.innerHTML = "📡 正在抓取标准答案..."; GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Authorization": token, "User-Agent": navigator.userAgent }, onload: function(response) { try { const json = JSON.parse(response.responseText); if (json.result) { API_ANSWERS = {}; json.result.forEach((q, index) => { if(q.id) API_ANSWERS[q.id] = q.correctAnswer; API_ANSWERS[`INDEX_${index}`] = q.correctAnswer; }); HAS_FETCHED_API = true; if(statusEl) statusEl.innerHTML = `✅ 成功获取 ${json.result.length} 题答案`; if(callback) callback(true); } else { if(callback) callback(false); } } catch (e) { if(callback) callback(false); } }, onerror: () => { if(callback) callback(false); } }); } // ================= UI 创建 ================= function createUI() { if (document.getElementById('ai-panel')) return; // 检查是否配置了 API const hasApiConfig = !!GM_getValue('UL_AI_URL', ''); const apiStatus = hasApiConfig ? "✅ AI已配置" : "⚪ 仅客观题模式"; const panel = document.createElement('div'); panel.id = 'ai-panel'; panel.innerHTML = `
🤖 优学院 V14.9
当前状态: ${apiStatus}
等待操作...
Author: Winds
Ref: 墨青
`; document.body.appendChild(panel); const actionBtn = panel.querySelector('#btn-action'); const pauseBtn = panel.querySelector('#btn-pause'); const stopBtn = panel.querySelector('#btn-stop'); const minimizeBtn = panel.querySelector('#btn-minimize'); const settingsBtn = panel.querySelector('#btn-settings'); // 最小化 minimizeBtn.onclick = (e) => { e.stopPropagation(); panel.classList.add('minimized'); }; let isDragAction = false; panel.addEventListener('mousedown', () => { isDragAction = false; }); panel.addEventListener('mousemove', () => { isDragAction = true; }); panel.addEventListener('click', () => { if (panel.classList.contains('minimized') && !isDragAction) panel.classList.remove('minimized'); }); // 设置按钮 settingsBtn.onclick = () => { const current = GM_getValue('UL_AI_URL', ''); const newUrl = prompt("配置 AI API 地址:\n留空则只做客观题。", current); if (newUrl !== null) { GM_setValue('UL_AI_URL', newUrl.trim()); location.reload(); } }; pauseBtn.onclick = togglePause; stopBtn.onclick = stopQueue; if (isListPage()) { GM_setValue('UL_LIST_URL', window.location.href); actionBtn.innerText = "▶ 队列"; actionBtn.onclick = startListQueue; if (GM_getValue('UL_QUEUE_MODE', false)) { actionBtn.style.display = 'none'; stopBtn.style.display = 'inline-block'; setTimeout(processListPage, 2000); } } else if (isResultPage()) { document.querySelector('#status-text').innerText = "✅ 作业已完成"; actionBtn.innerText = "↩️ 返回"; actionBtn.style.background = "#27ae60"; actionBtn.onclick = goBackToList; if (GM_getValue('UL_QUEUE_MODE', false)) { document.querySelector('#status-text').innerText = "3秒后自动返回列表..."; setTimeout(goBackToList, 3000); } } else if (isPotentialQuizPage()) { actionBtn.innerText = "▶ 答题"; actionBtn.onclick = () => startQuiz(false); setTimeout(() => startQuiz(true), 1500); } const header = panel.querySelector('#ai-header'); let isDragging = false, startX, startY, initLeft, initTop; header.onmousedown = (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); initLeft = rect.left; initTop = rect.top; }; document.onmousemove = (e) => { if(isDragging) { panel.style.left = (initLeft + e.clientX - startX) + 'px'; panel.style.top = (initTop + e.clientY - startY) + 'px'; } }; document.onmouseup = () => isDragging = false; } // ================= 返航逻辑 ================= function goBackToList() { const savedUrl = GM_getValue('UL_LIST_URL'); if (savedUrl && savedUrl.includes('ulearning')) { window.location.href = savedUrl; return; } const backBtn = document.querySelector(CONFIG.selectors.headerBackBtn); if (backBtn) { backBtn.click(); return; } if (document.referrer && document.referrer.includes('ulearning')) { window.location.href = document.referrer; return; } window.location.href = "https://www.ulearning.cn/amooc/user/student/homework"; } // ================= 暂停/停止控制 ================= function togglePause() { isPaused = !isPaused; const btn = document.querySelector('#btn-pause'); const status = document.querySelector('#status-text'); if (isPaused) { btn.innerText = "▶"; btn.style.background = "#27ae60"; status.innerHTML = `⏸ 已暂停`; } else { btn.innerText = "⏸"; btn.style.background = ""; status.innerHTML = "⚡ 运行中..."; if (isListPage()) processListPage(); else processNextQuizItem(); } } function stopQueue() { isRunning = false; isPaused = false; GM_setValue('UL_QUEUE_MODE', false); document.querySelector('#status-text').innerText = "❌ 已完全停止"; document.querySelector('#btn-action').style.display = 'inline-block'; document.querySelector('#btn-stop').style.display = 'none'; document.querySelector('#btn-pause').style.display = 'none'; if (isListPage()) { const actionBtn = document.querySelector('#btn-action'); actionBtn.innerText = "▶ 队列"; actionBtn.style.display = 'inline-block'; actionBtn.onclick = startListQueue; } } // ================= 队列管理 (列表页) ================= function startListQueue() { GM_setValue('UL_LIST_URL', window.location.href); GM_setValue('UL_QUEUE_MODE', true); location.reload(); } function processListPage() { if (!isListPage() || !GM_getValue('UL_QUEUE_MODE', false)) return; if (isPaused) return; document.querySelector('#btn-action').style.display = 'none'; document.querySelector('#btn-stop').style.display = 'inline-block'; document.querySelector('#btn-pause').style.display = 'inline-block'; GM_setValue('UL_LIST_URL', window.location.href); const logDiv = document.querySelector('#ai-log'); if(logDiv) logDiv.innerHTML = '
正在扫描作业列表...
'; const btns = Array.from(document.querySelectorAll(CONFIG.selectors.writeBtn)); const todoBtns = btns.filter(btn => { const txt = btn.innerText; if (!txt.includes("写作业") && !txt.includes("继续")) return false; // 智能避让逻辑:无AI时跳过主观题 if (!CONFIG.aiBaseUrl) { const row = btn.closest('.tr') || btn.closest('li'); const titleEl = row ? row.querySelector(CONFIG.selectors.itemName) : null; if (titleEl && titleEl.innerText.includes("主观题")) { if(logDiv) logDiv.innerHTML += `
🚫 无AI配置,跳过: ${titleEl.innerText}
`; return false; } } return true; }); if (todoBtns.length > 0) { document.querySelector('#status-text').innerText = `发现 ${todoBtns.length} 个可做作业...`; setTimeout(() => { if (!isPaused) todoBtns[0].click(); }, 3000); } else { const nextBtn = document.querySelector(CONFIG.selectors.nextPageBtn); if (nextBtn && !nextBtn.classList.contains('disabled') && nextBtn.style.display !== 'none') { document.querySelector('#status-text').innerText = "翻页中..."; setTimeout(() => { if(!isPaused) { nextBtn.click(); setTimeout(processListPage, 3000); } }, 2000); } else { document.querySelector('#status-text').innerText = "🎉 全部完成!"; stopQueue(); } } } // ================= 自动提交检测 ================= function checkAndSubmit() { const progressEl = document.querySelector(CONFIG.selectors.bottomBarNum); let isFinished = false; if (progressEl) { const match = progressEl.innerText.match(/(\d+)\s*\/\s*(\d+)/); if (match) { const done = parseInt(match[1]); const total = parseInt(match[2]); if (done >= total && total > 0) isFinished = true; } } if (isFinished) { const logDiv = document.querySelector('#ai-log'); logDiv.innerHTML += `
所有题目已完成,提交中...
`; const allBtns = Array.from(document.querySelectorAll(CONFIG.selectors.submitBtn)); const submitBtn = allBtns.find(b => b.innerText.includes("提交")); if (submitBtn) { submitBtn.click(); setTimeout(() => { const confirmBtns = Array.from(document.querySelectorAll(CONFIG.selectors.modalConfirmBtn)); const realConfirm = confirmBtns.find(b => b.innerText.includes("确定") || b.innerText.includes("提交")); if (realConfirm) { realConfirm.click(); logDiv.innerHTML += `
已确认。等待跳转...
`; setTimeout(goBackToList, 5000); } }, 1000); return true; } } return false; } // ================= 答题页流程 ================= function startQuiz(isAuto = false) { const startBtns = Array.from(document.querySelectorAll(CONFIG.selectors.startQuizBtn)); const realStartBtn = startBtns.find(b => b.innerText.includes("开始答题")); if (realStartBtn) { document.querySelector('#status-text').innerText = "👇 自动点击开始..."; realStartBtn.click(); setTimeout(() => startQuiz(isAuto), 2000); return; } questionsList = Array.from(document.querySelectorAll(CONFIG.selectors.questionContainer)); if (questionsList.length === 0) { if(!isAuto) alert("未找到题目"); return; } isRunning = true; isPaused = false; currentIndex = 0; document.querySelector('#btn-action').style.display = 'none'; document.querySelector('#btn-pause').style.display = 'inline-block'; document.querySelector('#btn-stop').style.display = 'inline-block'; fetchStandardAnswers((success) => { if (!success) document.querySelector('#ai-log').innerHTML += `
⚠️ API 获取失败,尝试 AI 模式
`; processNextQuizItem(); }); } function processNextQuizItem() { if (!isRunning) return; if (isPaused) return; if (currentIndex >= questionsList.length) { const submitted = checkAndSubmit(); if (!submitted) { document.querySelector('#status-text').innerText = "本页遍历结束"; setTimeout(goBackToList, 2000); } return; } const currentContainer = questionsList[currentIndex]; currentContainer.scrollIntoView({ behavior: "smooth", block: "center" }); questionsList.forEach(q => q.classList.remove('current-q')); currentContainer.classList.add('current-q'); document.querySelector('#status-text').innerHTML = `正在处理第 ${currentIndex + 1} / ${questionsList.length} 题`; if (isQuestionAnswered(currentContainer)) { currentIndex++; setTimeout(processNextQuizItem, 500); return; } const apiResult = getApiAnswerForQuestion(currentIndex, currentContainer); if (apiResult) { const logDiv = document.querySelector('#ai-log'); logDiv.innerHTML = `API ${apiResult.join(', ')}`; const answerStr = apiResult.join(" "); if (apiResult[0] === 'true' || apiResult[0] === 'false') autoSelectJudge(apiResult[0], currentContainer); else autoSelectChoices(answerStr, currentContainer); currentIndex++; setTimeout(() => processNextQuizItemWithDelay(1000), 50); } else { const logDiv = document.querySelector('#ai-log'); // 答题页智能避让:无AI配置且无API答案 if (!CONFIG.aiBaseUrl) { logDiv.innerHTML = `🚫 无AI配置且无API,跳过...`; currentIndex++; setTimeout(processNextQuizItem, 1000); return; } logDiv.innerHTML = `AI 思考中...`; callAI(currentContainer, (aiAnswer) => { const clean = cleanMarkdown(aiAnswer); logDiv.innerHTML = `
${clean}
`; autoFillText(clean, currentContainer); currentIndex++; processNextQuizItemWithDelay(3000); }); } } function processNextQuizItemWithDelay(delayMs) { let timeLeft = delayMs / 1000; const timer = setInterval(() => { if (!isRunning) { clearInterval(timer); return; } if (isPaused) { clearInterval(timer); document.querySelector('#status-text').innerHTML = `⏸ 已暂停`; return; } if (timeLeft <= 0) { clearInterval(timer); processNextQuizItem(); } else { timeLeft -= 0.5; } }, 500); } // ================= 辅助函数 ================= function getApiAnswerForQuestion(index, container) { if (!HAS_FETCHED_API) return null; const answers = API_ANSWERS[`INDEX_${index}`]; if (answers && answers.length > 0) return answers; return null; } function callAI(container, callback) { const info = extractPageInfo(container); if (info.error) { callback(""); return; } GM_xmlhttpRequest({ method: "POST", url: CONFIG.aiBaseUrl, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ prompt: info.prompt }), responseType: 'text', onload: function(response) { const chunks = response.responseText.split('data: '); let fullText = ""; chunks.forEach(c => { if(!c.trim() || c.includes('[DONE]')) return; try { fullText += JSON.parse(c).choices[0].delta.content || ""; } catch(e){} }); callback(fullText); }, onerror: () => callback("") }); } function isQuestionAnswered(container) { const textInput = container.querySelector(CONFIG.selectors.textInput); if (textInput && textInput.value.trim() !== "") return true; const checkedItems = container.querySelectorAll(CONFIG.selectors.checkedSelector); return checkedItems.length > 0; } function cleanMarkdown(text) { if (!text) return ""; return text.replace(/\*\*/g, "").replace(/#/g, "").trim(); } function extractPageInfo(container) { const qElem = container.querySelector(CONFIG.selectors.question); if (!qElem) return { error: "no question" }; let prompt = `题目:${qElem.innerText.trim()}\n这是一个填空题,请直接给出答案,不要解释。`; return { prompt, type: 'text' }; } function autoFillText(text, container) { const input = container.querySelector(CONFIG.selectors.textInput); if (input) { input.value = text; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('blur', { bubbles: true })); } } function autoSelectJudge(ans, container) { const isTrue = (ans === 'true' || ans.includes('正确')); const isFalse = (ans === 'false' || ans.includes('错误')); if (isTrue) container.querySelector(CONFIG.selectors.judgeTrueIcon)?.closest('.ul-radio')?.click(); else if (isFalse) container.querySelector(CONFIG.selectors.judgeFalseIcon)?.closest('.ul-radio')?.click(); } async function autoSelectChoices(ansStr, container) { const choices = container.querySelectorAll(CONFIG.selectors.choiceItem); const target = ansStr.toUpperCase(); for (let i = 0; i < choices.length; i++) { const choice = choices[i]; const idx = choice.querySelector(CONFIG.selectors.choiceIndex)?.innerText.trim().replace('.',''); if (idx && target.includes(idx)) { const area = choice.querySelector(CONFIG.selectors.choiceClickArea); if (area && !area.classList.contains('is-checked') && !choice.querySelector('input:checked')) { area.click(); await new Promise(r => setTimeout(r, 300)); } } } } window.addEventListener('load', () => { setTimeout(() => { createUI(); checkFirstRun(); }, 2000); }); })();