// ==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 = `
当前状态: ${apiStatus}
等待操作...
`;
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);
});
})();