新增平顶山学院作业答案助手脚本,支持 DeepSeek API,优化答案展示和配置设置
This commit is contained in:
parent
d5100655ee
commit
590229275c
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue