// ==UserScript==
// @name NEU MOOC 智能答题助手 (GitHub Release)
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 功能集大成版。包含AI答题、多选等待、可靠的自动停止机制、SweetAlert2美化弹窗、可拖动/悬浮球最小化面板,并已配置GitHub自动更新。
// @author LuBanQAQ
// @license MIT
// @match https://neustudydl.neumooc.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_getResourceText
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @resource sweetalert2_css https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css
// @connect *
// @downloadURL https://update.greasyfork.icu/scripts/538664/NEU%20MOOC%20%E6%99%BA%E8%83%BD%E7%AD%94%E9%A2%98%E5%8A%A9%E6%89%8B%20%28GitHub%20Release%29.user.js
// @updateURL https://update.greasyfork.icu/scripts/538664/NEU%20MOOC%20%E6%99%BA%E8%83%BD%E7%AD%94%E9%A2%98%E5%8A%A9%E6%89%8B%20%28GitHub%20Release%29.meta.js
// ==/UserScript==
(function () {
"use strict";
// --- 配置区 ---
const selectors = {
questionBox: ".item-box",
questionText: ".qusetion-info > .info-item > .value",
optionLabel: ".choices > label.el-radio, .choices > label.el-checkbox",
optionText:
".el-radio__label .choices-html, .el-checkbox__label .choices-html",
prevButton: ".left-bottom button:first-of-type",
nextButton: ".left-bottom button:last-of-type",
submitButton: ".infoCellRight .el-button--primary",
examContainer: ".respondPaperContainer",
answerCardNumbers: ".right-box .q-num-box",
activeAnswerCardNumber: ".right-box .q-num-box.is-q-active",
};
// --- AI 配置 ---
let aiConfig = {
apiKey: GM_getValue("apiKey", ""),
apiEndpoint: GM_getValue(
"apiEndpoint",
"https://api.openai.com/v1/chat/completions"
),
model: GM_getValue("model", "gpt-3.5-turbo"),
};
let isAutoAnswering = false;
// --- GUI 样式 ---
GM_addStyle(`
#control-panel { position: fixed; top: 150px; right: 20px; width: 320px; background-color: #f1f1f1; border: 1px solid #d3d3d3; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); z-index: 100000; font-family: Arial, sans-serif; color: #333; }
#control-panel-header { padding: 10px; cursor: move; background-color: #245FE6; color: white; border-top-left-radius: 8px; border-top-right-radius: 8px; display: flex; justify-content: space-between; align-items: center; }
#control-panel-body { padding: 15px; display: block; max-height: 70vh; overflow-y: auto; }
#control-panel-body.minimized { display: none; }
#control-panel button { display: block; width: 100%; padding: 8px 12px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; background-color: #fff; cursor: pointer; text-align: left; font-size: 13px; }
#control-panel button:hover { background-color: #e9e9e9; }
#control-panel .btn-primary { background-color: #245FE6; color: white; border-color: #245FE6; }
#control-panel .btn-danger { background-color: #dc3545; color: white; border-color: #dc3545; }
#control-panel .btn-info { background-color: #17a2b8; color: white; border-color: #17a2b8; }
#control-panel input[type="text"] { width: 100%; padding: 6px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
#log-area { margin-top: 10px; padding: 8px; height: 120px; overflow-y: auto; background-color: #fff; border: 1px solid #ddd; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; }
#minimize-btn { cursor: pointer; font-weight: bold; font-size: 18px; }
.collapsible-header { cursor: pointer; font-weight: bold; margin-top: 10px; padding-bottom: 5px; border-bottom: 1px solid #ccc; }
.collapsible-content { display: none; padding-top: 10px; }
.collapsible-content.visible { display: block; }
`);
// --- 创建 GUI ---
const panel = document.createElement("div");
panel.id = "control-panel";
panel.innerHTML = `
`;
document.body.appendChild(panel);
document.getElementById("api-key-input").value = GM_getValue("apiKey", "");
document.getElementById("api-endpoint-input").value = GM_getValue(
"apiEndpoint",
"https://api.openai.com/v1/chat/completions"
);
document.getElementById("model-input").value = GM_getValue(
"model",
"gpt-3.5-turbo"
);
const log = (message) => {
const logArea = document.getElementById("log-area");
if (logArea) {
logArea.innerHTML += `${new Date().toLocaleTimeString()}: ${message}
`;
logArea.scrollTop = logArea.scrollHeight;
}
};
// --- GUI 事件绑定 ---
document.querySelectorAll(".collapsible-header").forEach((header) => {
header.addEventListener("click", () =>
header.nextElementSibling.classList.toggle("visible")
);
});
document.getElementById("save-config-btn").addEventListener("click", () => {
aiConfig.apiKey = document.getElementById("api-key-input").value.trim();
aiConfig.apiEndpoint = document
.getElementById("api-endpoint-input")
.value.trim();
aiConfig.model = document.getElementById("model-input").value.trim();
GM_setValue("apiKey", aiConfig.apiKey);
GM_setValue("apiEndpoint", aiConfig.apiEndpoint);
GM_setValue("model", aiConfig.model);
log("✅ AI配置已保存。");
});
let isDragging = false,
offsetX,
offsetY;
const panelHeader = document.getElementById("control-panel-header");
panelHeader.addEventListener("mousedown", (e) => {
isDragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
panel.style.left = `${e.clientX - offsetX}px`;
panel.style.top = `${e.clientY - offsetY}px`;
}
});
document.addEventListener("mouseup", () => {
isDragging = false;
document.body.style.userSelect = "auto";
});
document.getElementById("minimize-btn").addEventListener("click", (e) => {
e.target.parentElement.nextElementSibling.classList.toggle("minimized");
e.target.textContent = e.target.textContent === "—" ? "❏" : "—";
});
// =================================================================
// 核心修改部分:修正 clickButton 函数
// =================================================================
const clickButton = (selector, logMsg, errorMsg) => {
const button = document.querySelector(selector);
// 增加检查:按钮必须存在、未被禁用,并且样式上是可见的
if (
button &&
!button.disabled &&
window.getComputedStyle(button).display !== "none"
) {
button.click();
log(logMsg);
return true;
}
log(errorMsg);
return false;
};
document
.getElementById("test-prev-btn")
.addEventListener("click", () =>
clickButton(
selectors.prevButton,
"点击了“上一题”。",
"未找到“上一题”按钮。"
)
);
document
.getElementById("test-next-btn")
.addEventListener("click", () =>
clickButton(
selectors.nextButton,
"点击了“下一题”。",
"未找到“下一题”按钮。"
)
);
document.getElementById("copy-question-btn").addEventListener("click", () => {
const questionBox = document.querySelector(
`${selectors.questionBox}:not([style*="display: none"])`
);
if (!questionBox) {
log("❌ 未找到题目。");
return;
}
const questionTitleElement = questionBox.querySelector(
selectors.questionText
);
if (!questionTitleElement) {
log("❌ 未找到题目正文。");
return;
}
const questionText = questionTitleElement.innerText.trim();
const options = Array.from(
questionBox.querySelectorAll(selectors.optionLabel)
);
let formattedString = `【题目】\n${questionText}\n\n【选项】\n`;
options.forEach((opt, i) => {
const letter = String.fromCharCode(65 + i);
const text = opt.querySelector(selectors.optionText)?.innerText.trim();
formattedString += `${letter}. ${text}\n`;
});
navigator.clipboard.writeText(formattedString).then(
() => log("✅ 当前题目已复制到剪贴板!"),
(err) => log("❌ 复制失败: " + err)
);
});
// --- AI 相关核心功能 ---
const getAiAnswer = (questionBox) => {
return new Promise((resolve, reject) => {
aiConfig.apiKey = GM_getValue("apiKey", "");
if (!aiConfig.apiKey) {
log("❌ 错误:请先配置API Key。");
return reject("API Key not set");
}
const questionTitleElement = questionBox.querySelector(
selectors.questionText
);
if (!questionTitleElement) return reject("无法解析题目正文。");
const questionText = questionTitleElement.innerText.trim();
const options = Array.from(
questionBox.querySelectorAll(selectors.optionLabel)
);
const isMultiple =
questionBox.querySelector(".el-checkbox-group") !== null;
if (options.length === 0) return reject("无法解析选项。");
let prompt = `你是一个严谨的答题助手。请根据以下题目和选项,找出最准确的答案。\n\n题目:${questionText}\n\n选项:\n`;
const optionMap = {};
options.forEach((opt, i) => {
const letter = String.fromCharCode(65 + i);
const text = opt.querySelector(selectors.optionText)?.innerText.trim();
prompt += `${letter}. ${text}\n`;
optionMap[letter] = text;
});
if (isMultiple) {
prompt += `\n注意:这是一个多选题,可能有一个或多个正确答案。请给出所有正确答案的字母,仅用逗号分隔(例如: A,B)。请只返回字母和逗号。`;
} else {
prompt += `\n注意:这是一个单选题。请只返回唯一正确答案的字母(例如: A)。`;
}
log(`💬 正在为题目 "${questionText.slice(0, 15)}..." 请求AI...`);
GM_xmlhttpRequest({
method: "POST",
url: aiConfig.apiEndpoint,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${aiConfig.apiKey}`,
},
data: JSON.stringify({
model: aiConfig.model,
messages: [{ role: "user", content: prompt }],
temperature: 0,
}),
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const aiAnswerRaw = data.choices[0].message.content;
log(`🤖 AI 返回: ${aiAnswerRaw}`);
const letters = aiAnswerRaw
.replace(/[^A-Z,]/g, "")
.split(",")
.filter(Boolean);
const answersText = letters
.map((l) => optionMap[l])
.filter(Boolean);
resolve(answersText);
} catch (e) {
reject("AI响应解析失败: " + e.message);
}
},
onerror: (res) => reject("AI请求失败: " + res.statusText),
});
});
};
async function selectOptionByText(questionBox, answer) {
const options = questionBox.querySelectorAll(selectors.optionLabel);
let found = false;
const answersToClick = Array.isArray(answer) ? answer : [answer];
const isMultipleWithDelay = answersToClick.length > 1;
for (const optionLabel of options) {
const optionTextElement = optionLabel.querySelector(selectors.optionText);
if (optionTextElement) {
const currentOptionText = optionTextElement.innerText.trim();
if (answersToClick.some((ans) => currentOptionText.includes(ans))) {
if (!optionLabel.classList.contains("is-checked")) {
optionLabel.click();
log(` - 已选择: ${currentOptionText}`);
found = true;
if (isMultipleWithDelay) {
log("多选题,等待1秒...");
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
}
}
return found;
}
document
.getElementById("ai-single-solve-btn")
.addEventListener("click", async () => {
const questionBox = document.querySelector(
`${selectors.questionBox}:not([style*="display: none"])`
);
if (!questionBox) {
log("❌ 未找到当前题目。");
return;
}
try {
log("正在请求AI解答本题...");
const answers = await getAiAnswer(questionBox);
if (answers && answers.length > 0) {
await selectOptionByText(questionBox, answers);
} else {
log("⚠️ AI未能提供有效答案。");
}
} catch (error) {
log(`❌ AI搜题出错: ${error}`);
}
});
// --- 全自动答题逻辑 ---
function isLastQuestion() {
const allNumbers = document.querySelectorAll(selectors.answerCardNumbers);
if (allNumbers.length === 0) return false;
const activeNumberEl = document.querySelector(
selectors.activeAnswerCardNumber
);
if (!activeNumberEl) return false;
const lastNumberEl = allNumbers[allNumbers.length - 1];
if (activeNumberEl.innerText.trim() === lastNumberEl.innerText.trim()) {
return true;
}
return false;
}
const fullAutoBtn = document.getElementById("full-auto-btn");
const stopAutoAnswering = () => {
isAutoAnswering = false;
fullAutoBtn.innerText = "⚡️ 开始全自动 AI 答题";
fullAutoBtn.classList.remove("btn-danger");
fullAutoBtn.classList.add("btn-primary");
log("🔴 全自动答题已停止。");
};
const runAutoAnswerStep = async () => {
if (!isAutoAnswering) return;
const questionBox = document.querySelector(
`${selectors.questionBox}:not([style*="display: none"])`
);
if (!questionBox) {
log("🏁 未找到题目,流程结束。");
stopAutoAnswering();
return;
}
try {
const answers = await getAiAnswer(questionBox);
if (!isAutoAnswering) return;
if (answers && answers.length > 0) {
await selectOptionByText(questionBox, answers);
} else {
log("⚠️ AI未能提供答案,跳过本题。");
}
} catch (error) {
log(`❌ AI搜题出错: ${error}`);
stopAutoAnswering();
return;
}
if (isLastQuestion()) {
log("🏁 已到达最后一题(答题卡判断),自动循环停止。");
stopAutoAnswering();
return;
}
const delay = 2500 + Math.random() * 1000;
log(`...等待 ${delay / 1000} 秒后进入下一题...`);
setTimeout(() => {
if (!isAutoAnswering) return;
const clickedNext = clickButton(
selectors.nextButton,
"自动点击“下一题”。",
"⚠️ 未找到或隐藏了“下一题”按钮。"
);
if (!clickedNext) {
log("🏁 已到达最后一题(按钮判断),自动循环停止。");
stopAutoAnswering();
} else {
setTimeout(runAutoAnswerStep, 1500);
}
}, delay);
};
fullAutoBtn.addEventListener("click", () => {
if (isAutoAnswering) {
stopAutoAnswering();
} else {
isAutoAnswering = true;
fullAutoBtn.innerText = "🛑 停止全自动答题";
fullAutoBtn.classList.remove("btn-primary");
fullAutoBtn.classList.add("btn-danger");
log("🟢 全自动答题已启动...");
runAutoAnswerStep();
}
});
})();