USTC 自动评教 tqm.ustc.edu.cn
// ==UserScript== // @name Auto grading // @namespace http://tampermonkey.net/ // @version 0.7.3 // @description USTC 自动评教 tqm.ustc.edu.cn // @author PRO_2684 // @match https://tqm.ustc.edu.cn/index.html* // @icon https://tqm.ustc.edu.cn/favicon.ico // @grant GM_getResourceText // @license gpl-3.0 // @resource answers https://cdn.jsdelivr.net/gh/PRO-2684/gadgets/auto_grading/answers.json // ==/UserScript== (function () { 'use strict'; const INTERVAL = 500; // ms const log = console.log.bind(console, "[Auto grading]"); const standard_answers = JSON.parse(GM_getResourceText("answers")); let bypass_timer = false; let menu_root; function clean(str) { // Remove spaces str = str.replace(/\s+/g, ""); // Remove leading asterisk if (str[0] == '*') str = str.slice(1); // Remove leading serial number str = str.replace(/^\d*\./, ""); // Remove "(单选题)"/"(多选题)" str = str.replace("(单选题)", ""); str = str.replace("(多选题)", ""); return str; } function on_bypass_click() { bypass_timer = !bypass_timer; this.textContent = `绕过倒计时 [${bypass_timer ? "✔" : "✘"}]`; } function add_item(display_name, hint, callback) { const new_item = menu_root.appendChild(document.createElement("li")); new_item.innerText = display_name; new_item.onclick = callback; new_item.className = "ant-menu-item"; new_item.title = hint; } function help() { alert("食用方法:\n1. 进入未完成的评价问卷\n2. 侧栏选择你想要的操作或激活快捷键\n3. 等待脚本执行\n\n快捷键说明:\n- Enter: 智能执行以下中的一项: 下一位教师/选择标准答案/提交回答\n- Shift+Enter: 全自动评教\n- Backspace: 忽略并转到下一个"); } function grade() { const questions = document.querySelectorAll("[class^='index_subject-']"); const disabled = questions[0].querySelector(".ant-radio-wrapper-disabled"); if (disabled) return false; let first_unchosen = null; questions.forEach((question) => { const required = Boolean(question.querySelector('[class^="index_necessary"]')); if (!required) return; const tmp = question.querySelector("[class^='index_title']"); const remark = tmp.querySelector("[class^='index_remarks-']"); const title = remark?.textContent || clean(tmp.querySelector("[class^='index_richTextContent']").textContent); const standard_answer = standard_answers[title]; log(`${title}: ${standard_answer}`); let chosen = false; if (standard_answer) { const options = question.querySelectorAll('[style="width: 100%;"]'); for (const option of options) { const is_standard_answer = (standard_answer.indexOf(option.innerText) >= 0); // const is_selected = option.querySelector(".ant-checkbox-checked") || option.querySelector(".ant-radio-checked"); if (is_standard_answer) { option.firstChild.click(); chosen = true; // break; // Compatible for multiple answers } } } if (!chosen && first_unchosen == null) first_unchosen = question; }); if (first_unchosen != null) { first_unchosen.scrollIntoView({ behavior: "smooth" }); return false; } return true; } function ignore() { const ignore_btn = root_node.querySelector("[class^='TaskDetailsMainContent_normalButton']"); if (ignore_btn && ignore_btn.parentElement.parentElement.parentElement.getAttribute('aria-hidden') == 'false') { ignore_btn.click(); } else { log("Cannot find ignore button!"); } const tabs = root_node.querySelector("[class='ant-tabs-nav-scroll']"); if (tabs) { tabs = tabs.children[0].children[0]; } else { log("Cannot find teacher/TA list!"); return; } let flag = false; let tab; for (tab of tabs.children) { if (flag) { tab.click(); break; } else if (tab.getAttribute('aria-selected') == 'true') { flag = true; } } } async function auto() { if (await try_click("button[class^='ant-btn ant-btn-primary']")) // Confirm submission / Next teacher or course return true; if (grade()) { // Select standard answer await try_click("button[class^='ant-btn index_submit']"); // Submit return true; } return false; } async function full_auto() { // Wait INTERVAL ms between auto() resolves and next auto() call while (await auto()) { await new Promise((resolve) => setTimeout(resolve, INTERVAL)); } alert("Success!"); } function dump() { const questions = document.querySelectorAll("[class^='index_subject-']"); const disabled = questions[0].querySelector(".ant-radio-wrapper-disabled"); if (disabled) return false; let data = {}; questions.forEach((question) => { const required = Boolean(question.querySelector('[class^="index_necessary"]')); if (!required) return; const tmp = question.querySelector("[class^='index_title']"); const remark = tmp.querySelector("[class^='index_remarks-']"); const title = remark?.textContent || clean(tmp.querySelector("[class^='index_richTextContent']").textContent); const options = question.querySelectorAll('[style="width: 100%;"]'); data[title] = []; for (const option of options) { data[title].push(option.innerText); } }); log(JSON.stringify(data)); } function is_displayed(ele) { let displayed = true; let node = ele; while (node) { if (node.style.display == "none") { displayed = false; break; } node = node.parentElement; } return displayed; } function force_enable(ele) { ele.removeAttribute("disabled"); const prefix = "__reactEventHandlers$"; for (const key of Object.getOwnPropertyNames(ele)) { if (key.startsWith(prefix)) { ele[key].disabled = false; } } } async function until_enabled(ele) { return new Promise((resolve) => { if (!ele.hasAttribute("disabled")) { resolve(); return; } else if (bypass_timer) { force_enable(ele); resolve(); return; } log("Waiting for button to be enabled...", ele); const observer = new MutationObserver((mutations, observer) => { if (!ele.hasAttribute("disabled")) { observer.disconnect(); log("Button is enabled!", ele); resolve(); } }); observer.observe(ele, { attributes: true }); }); } async function try_click(selector) { const eles = document.querySelectorAll(selector); for (const ele of eles) { if (ele && is_displayed(ele)) { await until_enabled(ele); ele.click(); return true; } } return false; } // Side bar const root_node = document.getElementById('root'); const config = { attributes: false, childList: true, subtree: true }; const callback = function (mutations, observer) { menu_root = root_node.querySelector('.ant-menu-root'); if (menu_root) { observer.disconnect(); add_item("使用说明", "自动评教脚本使用说明", help); add_item("绕过倒计时 [✘]", "(实验性功能)在 Enter 以及全自动评教时绕过 5 秒倒计时", on_bypass_click); add_item("自动评价", "自动选择标准答案", grade); add_item("忽略并转到下一个", "(若可能)忽略当前助教并转到下一个助教", ignore); add_item("全自动评教", "(实验性功能)彻底解放双手", full_auto); add_item("输出答案", "(调试用)输出当前问卷的所有答案", dump); } } const observer = new MutationObserver(callback); observer.observe(root_node, config); // Shortcut document.addEventListener("keyup", (e) => { if (document.activeElement.nodeName != "INPUT" || document.activeElement.nodeName != "TEXTAREA") { // Don't trigger when typing switch (e.key) { case "Enter": if (!e.shiftKey) { auto(); } else { full_auto(); } break; case "Backspace": ignore(); break; default: break; } } }); })();