From 590229275c8a7fa4be579b6ed641a4af70d037fc Mon Sep 17 00:00:00 2001 From: jinglekang Date: Fri, 5 Jun 2026 16:55:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B9=B3=E9=A1=B6=E5=B1=B1?= =?UTF-8?q?=E5=AD=A6=E9=99=A2=E4=BD=9C=E4=B8=9A=E7=AD=94=E6=A1=88=E5=8A=A9?= =?UTF-8?q?=E6=89=8B=E8=84=9A=E6=9C=AC=EF=BC=8C=E6=94=AF=E6=8C=81=20DeepSe?= =?UTF-8?q?ek=20API=EF=BC=8C=E4=BC=98=E5=8C=96=E7=AD=94=E6=A1=88=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=92=8C=E9=85=8D=E7=BD=AE=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qingshuxuetang_homework_answer.user.js | 424 +++++++++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 qingshuxuetang_homework_answer.user.js diff --git a/qingshuxuetang_homework_answer.user.js b/qingshuxuetang_homework_answer.user.js new file mode 100644 index 0000000..9b33af0 --- /dev/null +++ b/qingshuxuetang_homework_answer.user.js @@ -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(); + } + }); +})();