新增平顶山学院作业答案助手脚本,支持 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