// ==UserScript== // @name 平顶山学院(青书学堂)作业答案助手 // @namespace http://tampermonkey.net/ // @version 2026-06-05.2 // @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 htmlToText = (html = "") => { const element = document.createElement("div"); element.innerHTML = html; return element.textContent.trim(); }; const parseAnswerLabels = (solution = "") => { const text = htmlToText(solution).toUpperCase(); const labels = text.match(/[A-Z]/g) || []; return Array.from(new Set(labels)); }; const formatOfficialAnswer = (questionData) => { const solution = questionData.solution || ""; const answerLabels = parseAnswerLabels(solution); if (answerLabels.length === 0 || !questionData.options?.length) { return htmlToText(solution) || solution; } const optionMap = new Map( questionData.options.map((option) => [ String(option.label || "").trim().toUpperCase(), htmlToText(option.description), ]) ); const matchedAnswers = answerLabels.map((label) => { const optionText = optionMap.get(label); return optionText ? label + ". " + optionText : label; }); return matchedAnswers.join("\n"); }; 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( "官方答案", formatOfficialAnswer(questionData), "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(); } }); })();