新增平顶山学院作业答案助手脚本,支持 DeepSeek API,优化答案展示和配置设置

This commit is contained in:
竟康 2026-06-05 16:55:19 +08:00
parent d5100655ee
commit 590229275c
1 changed files with 424 additions and 0 deletions

View File

@ -0,0 +1,424 @@
// ==UserScript==
// @name 平顶山学院(青书学堂)作业答案助手
// @namespace http://tampermonkey.net/
// @version 2026-06-05.1
// @description 在青书学堂作业页面展示官方答案,缺少官方答案时使用 DeepSeek 生成参考答案
// @author JK
// @license MIT
// @match https://degree.qingshuxuetang.com/*/Student/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=qingshuxuetang.com
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @connect api.deepseek.com
// @connect degree.qingshuxuetang.com
// ==/UserScript==
(function () {
"use strict";
const CONFIG_STORAGE_KEY = "qingshu_homework_answer_config";
const ANSWER_CLASS = "qingshu-homework-answer";
const STYLE_ID = "qingshu-homework-answer-style";
const DEFAULT_CONFIG = {
deepseekApiKey: "",
deepseekModel: "deepseek-chat",
deepseekApiUrl: "https://api.deepseek.com/chat/completions",
autoShowAnswers: true,
};
const CONFIG = {
...DEFAULT_CONFIG,
...GM_getValue(CONFIG_STORAGE_KEY, {}),
};
const saveConfig = (patch) => {
Object.assign(CONFIG, patch);
GM_setValue(CONFIG_STORAGE_KEY, CONFIG);
alert("设置已保存,刷新页面后生效");
};
const registerConfigMenus = () => {
GM_registerMenuCommand("设置 DeepSeek API Key", () => {
const input = prompt(
"请输入 DeepSeek API Key",
CONFIG.deepseekApiKey ? "已设置,输入新值可覆盖" : ""
);
if (input === null) {
return;
}
const apiKey = input.trim();
if (!apiKey) {
alert("API Key 不能为空");
return;
}
saveConfig({ deepseekApiKey: apiKey });
});
GM_registerMenuCommand("设置 DeepSeek 模型", () => {
const input = prompt("请输入 DeepSeek 模型名", CONFIG.deepseekModel);
if (input === null) {
return;
}
const model = input.trim();
if (!model) {
alert("模型名不能为空");
return;
}
saveConfig({ deepseekModel: model });
});
GM_registerMenuCommand("设置是否自动显示答案", () => {
const input = prompt(
"是否自动显示答案?输入 true 或 false",
String(CONFIG.autoShowAnswers)
);
if (input === null) {
return;
}
saveConfig({ autoShowAnswers: input.trim().toLowerCase() === "true" });
});
GM_registerMenuCommand("手动显示作业答案", () => {
showAnswers();
});
GM_registerMenuCommand("恢复默认设置", () => {
GM_deleteValue(CONFIG_STORAGE_KEY);
Object.assign(CONFIG, DEFAULT_CONFIG);
alert("设置已重置,刷新页面后生效");
});
};
const ensureStyle = () => {
if (document.querySelector("#" + STYLE_ID)) {
return;
}
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
.${ANSWER_CLASS} {
margin: 12px 0;
padding: 12px 14px;
border-left: 4px solid #2563eb;
border-radius: 6px;
background: #eff6ff;
color: #1f2937;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
}
.${ANSWER_CLASS}.official {
border-left-color: #16a34a;
background: #f0fdf4;
}
.${ANSWER_CLASS}.ai {
border-left-color: #7c3aed;
background: #f5f3ff;
}
.${ANSWER_CLASS}.error {
border-left-color: #dc2626;
background: #fef2f2;
}
.${ANSWER_CLASS} .answer-title {
margin-bottom: 6px;
font-weight: 700;
}
`;
(document.head || document.documentElement).appendChild(style);
};
const getUrlParams = () => {
const url = new URL(window.location.href);
return {
quizId: url.searchParams.get("quizId"),
courseId: url.searchParams.get("courseId"),
teachPlanId: url.searchParams.get("teachPlanId"),
periodId: url.searchParams.get("periodId"),
};
};
const getSchoolCode = () => {
const [, schoolCode] = location.pathname.split("/");
return schoolCode || "pdsu";
};
const waitForCondition = (getResult, timeoutMs = 30000) => {
const result = getResult();
if (result) {
return Promise.resolve(result);
}
return new Promise((resolve) => {
let timeoutId;
const observer = new MutationObserver(() => {
const result = getResult();
if (result) {
observer.disconnect();
clearTimeout(timeoutId);
resolve(result);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
timeoutId = setTimeout(() => {
observer.disconnect();
resolve(null);
}, timeoutMs);
});
};
const fetchQuizData = async (quizId) => {
const timestamp = Date.now();
const url =
"https://degree.qingshuxuetang.com/" +
getSchoolCode() +
"/Student/DetailData?quizId=" +
encodeURIComponent(quizId) +
"&_=" +
timestamp +
"&_t=" +
(timestamp + 2000);
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json, text/javascript, */*; q=0.01",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
},
credentials: "include",
});
return response.json();
};
const parseDescription = (description = "") => {
const element = document.createElement("div");
element.innerHTML = description;
return {
text: element.textContent.trim(),
imageUrls: Array.from(element.querySelectorAll("img"))
.map((image) => image.src)
.filter(Boolean),
};
};
const getQuestionTypeName = (typeId) => {
if (typeId === 1) {
return "单选题";
}
if (typeId === 2) {
return "多选题";
}
if (typeId === 3) {
return "简答题";
}
return "未知题型";
};
const buildQuestionPrompt = (questionData) => {
const { text, imageUrls } = parseDescription(questionData.description);
const options = (questionData.options || []).map(
(option) => option.label + ". " + option.description
);
const typeName = getQuestionTypeName(questionData.typeId);
return [
"请根据题目给出参考答案。",
"题型:" + typeName,
"要求:选择题只输出正确选项和简要理由;简答题输出简洁答案。",
imageUrls.length > 0
? "注意:题目包含图片,但当前脚本暂未发送图片,请根据文字和选项作答。"
: "",
"",
"题目:",
text,
options.length > 0 ? "\n选项\n" + options.join("\n") : "",
]
.filter(Boolean)
.join("\n");
};
const requestDeepSeek = (questionData) =>
new Promise((resolve, reject) => {
if (!CONFIG.deepseekApiKey) {
reject(new Error("未设置 DeepSeek API Key"));
return;
}
GM_xmlhttpRequest({
method: "POST",
url: CONFIG.deepseekApiUrl,
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + CONFIG.deepseekApiKey,
},
data: JSON.stringify({
model: CONFIG.deepseekModel,
messages: [
{
role: "system",
content:
"你是作业参考答案助手。请直接给出简洁参考答案,不要输出无关寒暄。",
},
{
role: "user",
content: buildQuestionPrompt(questionData),
},
],
temperature: 0.2,
}),
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
const answer = data.choices?.[0]?.message?.content;
if (!answer) {
reject(new Error("DeepSeek 未返回答案"));
return;
}
resolve(answer);
} catch (error) {
reject(error);
}
},
onerror: () => {
reject(new Error("DeepSeek 请求失败"));
},
});
});
const createAnswerDisplay = (title, content, type) => {
const answerDisplay = document.createElement("div");
answerDisplay.className = ANSWER_CLASS + " " + type;
const titleElement = document.createElement("div");
titleElement.className = "answer-title";
titleElement.textContent = title;
const contentElement = document.createElement("div");
contentElement.textContent = content;
answerDisplay.appendChild(titleElement);
answerDisplay.appendChild(contentElement);
return answerDisplay;
};
const findQuestionElement = (questionId) =>
document.querySelector('[id="' + questionId + '"]');
const showAnswers = async () => {
ensureStyle();
document.querySelectorAll("." + ANSWER_CLASS).forEach((item) => item.remove());
const { quizId } = getUrlParams();
if (!quizId) {
alert("当前页面没有 quizId暂时无法获取作业数据");
return;
}
try {
const data = await fetchQuizData(quizId);
const questions = data?.data?.paperDetail?.questions || [];
if (questions.length === 0) {
alert("未获取到题目数据,请确认当前页面是否为作业页面");
return;
}
await waitForCondition(
() => questions.some((question) => findQuestionElement(question.questionId)),
30000
);
for (const questionData of questions) {
const questionElement = findQuestionElement(questionData.questionId);
if (!questionElement) {
continue;
}
if (questionData.solution) {
questionElement.appendChild(
createAnswerDisplay("官方答案", questionData.solution, "official")
);
continue;
}
if (questionData.userAnswer) {
questionElement.appendChild(
createAnswerDisplay("已作答", "当前题目已作答,未调用 AI。", "official")
);
continue;
}
const loadingDisplay = createAnswerDisplay(
"AI 参考答案",
"正在请求 DeepSeek...",
"ai"
);
questionElement.appendChild(loadingDisplay);
try {
const aiAnswer = await requestDeepSeek(questionData);
loadingDisplay.querySelector(".answer-title").textContent =
"DeepSeek 参考答案";
loadingDisplay.querySelector("div:last-child").textContent = aiAnswer;
} catch (error) {
loadingDisplay.className = ANSWER_CLASS + " error";
loadingDisplay.querySelector(".answer-title").textContent = "获取失败";
loadingDisplay.querySelector("div:last-child").textContent =
error.message || "DeepSeek 请求失败";
}
}
} catch (error) {
console.error("显示答案失败:", error);
alert("显示答案失败,请查看控制台日志");
}
};
registerConfigMenus();
window.addEventListener("load", () => {
const { quizId } = getUrlParams();
if (CONFIG.autoShowAnswers && quizId) {
showAnswers();
}
});
})();