// ==UserScript==
// @name QingJiaoHelper
// @namespace http://tampermonkey.net/
// @version 0.3.5.3
// @description 青骄第二课堂小助手: 2024 知识竞赛 | 跳过视频 | 自动完成所有课程 | 领取每日学分 | 课程自动填充答案
// @author FoliageOwO
// @match *://www.2-class.com/*
// @match *://2-class.com/*
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @license GPL-3.0
// @supportURL https://github.com/FoliageOwO/QingJiaoHelper
// @require https://fastly.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js
// @require https://update.greasyfork.org/scripts/453791/lib2class.js
// @require https://fastly.jsdelivr.net/npm/axios@1.3.6/dist/axios.min.js
// @resource toastifycss https://fastly.jsdelivr.net/npm/toastify-js/src/toastify.css
// @resource spectrecss https://fastly.jsdelivr.net/gh/FoliageOwO/QingJiaoHelper/spectre.css
// ==/UserScript==
const apiGetGradeLevels = {
method: "GET",
api: "/course/getHomepageGrade",
};
const apiGetCoursesByGradeLevel = {
method: "GET",
api: "/course/getHomepageCourseList?grade=${grade}&pageSize=50&pageNo=1",
};
const apiGetSelfCoursesByGradeLevel = {
method: "GET",
api: "/course/getHomepageCourseList?grade=自学&pageNo=1&pageSize=500&sort=&type=${grade}",
};
const apiGetTestPaperList = {
method: "GET",
api: "/exam/getTestPaperList?courseId=${courseId}",
};
const apiCommitExam = {
method: "POST",
api: "/exam/commit",
};
const apiAddMedal = {
method: "GET",
api: "/medal/addMedal",
};
const apiGetBeforeResourcesByCategoryName = {
method: "POST",
api: "/resource/getBeforeResourcesByCategoryName",
};
const apiAddPCPlayPV = {
method: "POST",
api: "/resource/addPCPlayPV",
};
const apiLikePC = {
method: "POST",
api: "/resource/likePC",
};
async function requestAPI(api, params, data) {
const method = api.method;
const origin = "https://www.2-class.com";
let url = `${origin}/api${api.api}`;
for (const key in params) {
url = url.replaceAll("${" + key + "}", params[key]);
}
if (method === "GET") {
return await axios({
method: "GET",
url,
})
.then((response) => {
const rdata = response.data;
console.debug(`[${method}] ${url}`, data, rdata);
if (rdata.success === false || rdata.data === null) {
const errorMessage = rdata.errorMsg;
const errorCode = rdata.errorCode;
console.error(`API 返回错误 [${errorCode}]:${errorMessage},请刷新页面重试!`);
return null;
}
else {
return rdata;
}
})
.catch((reason) => {
showMessage(`请求 API 失败(${reason.code}):${reason.message}\n请将控制台中的具体报错提交!`, "red");
console.error(`请求失败(${reason.status}/${reason.code})→${reason.message}→`, reason.toJSON(), reason.response, reason.stack);
});
}
else {
return await axios({
method: "POST",
url,
withCredentials: true,
headers: {
"Content-Type": "application/json;charset=UTF-8",
},
data,
}).then((response) => {
const rdata = response.data;
console.debug(`[${method}] ${url}`, data, rdata);
if (rdata.success === false || rdata.data === null) {
const errorMessage = rdata.errorMsg;
const errorCode = rdata.errorCode;
console.error(`API 返回错误 [${errorCode}]:${errorMessage},请刷新页面重试!`);
return null;
}
else {
return rdata;
}
});
}
}
async function getAvailableGradeLevels() {
return await requestAPI(apiGetGradeLevels).then((data) => {
return data ? data.data.map((it) => it.value) : null;
});
}
async function getCoursesByGradeLevel(gradeLevel) {
return await requestAPI(apiGetCoursesByGradeLevel, {
grade: gradeLevel,
}).then((data) => {
return data ? data.data.list : null;
});
}
async function getSelfCoursesByGradeLevel(gradeLevel) {
return await requestAPI(apiGetSelfCoursesByGradeLevel, {
grade: gradeLevel,
}).then((data) => {
return data ? data.data.list : null;
});
}
async function getTestPaperList(courseId) {
return await requestAPI(apiGetTestPaperList, { courseId }).then((data) => {
return data ? data.data.testPaperList : null;
});
}
async function getCourseAnswers(courseId) {
return await getTestPaperList(courseId).then((testPaperList) => {
if (!isNone(testPaperList)) {
const answers = testPaperList.map((column) => column.answer);
console.debug(`成功获取课程 [${courseId}] 的答案`, answers);
return answers.map((it) => it.split("").join(","));
}
else {
console.error(`无法获取课程 [${courseId}] 答案!`);
return null;
}
});
}
async function commitExam(data) {
return await requestAPI(apiCommitExam, {}, data);
}
async function addMedal() {
return await requestAPI(apiAddMedal).then((data) => {
if (isNone(data)) {
return null;
}
else {
const flag = data.flag;
const num = data.medalNum;
if (flag) {
return num;
}
else {
return undefined;
}
}
});
}
async function getBeforeResourcesByCategoryName(data) {
return await requestAPI(apiGetBeforeResourcesByCategoryName, {}, data).then((data) => data
? data.data.list.map((it) => {
return {
title: it.briefTitle,
resourceId: it.resourceId,
};
})
: null);
}
async function addPCPlayPV(data) {
return await requestAPI(apiAddPCPlayPV, {}, data).then((data) => {
return data ? data.data.result : null;
});
}
async function likePC(data) {
return await requestAPI(apiLikePC, {}, data).then((data) => {
if (isNone(data)) {
return null;
}
else {
const rdata = data.data;
return !Number.isNaN(Number(rdata)) || rdata.errorCode === "ALREADY_like";
}
});
}
const scriptName = "QingJiaoHelper";
const scriptVersion = "v0.3.5.3";
const toastifyDuration = 3 * 1000;
const toastifyGravity = "top";
const toastifyPosition = "left";
const fuzzyFindConfidenceTreshold = 0.8;
const __DATA__ = () => window["__DATA__"];
const reqtoken = () => __DATA__().reqtoken;
const userInfo = () => __DATA__().userInfo;
const isLogined = () => JSON.stringify(userInfo()) !== "{}";
const accountGradeLevel = () => isLogined() ? userInfo().department.gradeName : "未登录";
const coursesGradeLevels = async () => await getAvailableGradeLevels();
const selfCoursesGradeLevels = async () => [
"小学",
"初中",
"高中",
"中职",
"通用",
];
("use strict");
const isTaskCoursesEnabled = () => getGMValue("qjh_isTaskCoursesEnabled", false);
const isTaskSelfCourseEnabled = () => getGMValue("qjh_isTaskSelfCourseEnabled", false);
const isTaskGetCreditEnabled = () => getGMValue("qjh_isTaskGetCreditEnabled", false);
const isTaskFinalExaminationEnabled = () => getGMValue("qjh_isTaskFinalExaminationEnabled", false);
const isFullAutomaticEmulationEnabled = () => getGMValue("qjh_isFullAutomaticEmulationEnabled", false);
const isTaskCompetitionEnabled = () => getGMValue("qjh_isTaskCompetitionEnabled", true);
let autoComplete = () => featureNotAvailable("自动完成");
let autoCompleteCreditsDone = () => getGMValue("qjh_autoCompleteCreditsDone", false);
const features = [
{
key: "courses",
title: "自动完成所有课程(不包括考试)",
matcher: ["/courses", "/drugControlClassroom/courses"],
task: () => taskCourses(false),
enabled: isTaskCoursesEnabled,
},
{
key: "selfCourse",
title: "自动完成所有自学课程(不包括考试)",
matcher: ["/selfCourse", "/drugControlClassroom/selfCourse"],
task: () => taskCourses(true),
enabled: isTaskSelfCourseEnabled,
},
{
key: "credit",
title: "自动获取每日学分(会花费一段时间,请耐心等待)",
matcher: ["/admin/creditCenter"],
task: taskGetCredit,
enabled: isTaskGetCreditEnabled,
},
{
key: "singleCourse",
title: "单个课程自动填充答案",
matcher: /\/courses\/exams\/(\d+)/,
task: taskSingleCourse,
enabled: () => true,
},
{
key: "competition",
title: "知识竞赛",
matcher: ["/competition"],
task: taskCompetition,
enabled: isTaskCompetitionEnabled,
},
{
key: "finalExamination",
title: "期末考试",
matcher: ["/courses/exams/finalExam"],
task: taskFinalExamination,
enabled: isTaskFinalExaminationEnabled,
},
{
key: "skip",
title: "跳过课程视频",
matcher: /\/courses\/(\d+)/,
task: taskSkip,
enabled: () => true,
},
];
function triggerFeatures() {
if (location.pathname === "/") {
showMessage(`${scriptName}\n版本:${scriptVersion}`, "green");
}
features.forEach((feature) => {
let matcher = feature.matcher;
let isMatched = matcher instanceof RegExp
? location.pathname.match(matcher)
: matcher.indexOf(location.pathname) !== -1;
if (isMatched && feature.enabled()) {
showMessage(`激活功能:${feature.title}`, "green");
feature.task();
}
});
}
(function () {
for (let script of document.getElementsByTagName("script")) {
if (script.innerText.indexOf("window.__DATA__") !== -1) {
eval(script.innerText);
}
}
GM_addStyle(GM_getResourceText("toastifycss"));
GM_addStyle(GM_getResourceText("spectrecss"));
GM_registerMenuCommand("菜单", showMenu);
prepareMenu();
let pathname = location.pathname;
setInterval(() => {
const newPathName = location.pathname;
if (newPathName !== pathname) {
console.debug(`地址改变`, pathname, newPathName);
pathname = newPathName;
triggerFeatures();
}
});
triggerFeatures();
})();
const customGradeLevels = () => getGMValue("qjh_customGradeLevels", []);
const customSelfGradeLevels = () => getGMValue("qjh_customSelfGradeLevels", []);
async function prepareMenu() {
const menuElement = await waitForElementLoaded("#qjh-menu");
const coursesGradeLevelsList = await coursesGradeLevels();
const selfCoursesGradeLevelsList = await selfCoursesGradeLevels();
if (coursesGradeLevels === null || selfCoursesGradeLevelsList === null) {
showMessage(`课程年级列表或自学课程年级列表获取失败!`, "red");
}
const titleElement = await waitForElementLoaded("#qjh-menu-title");
titleElement.append(scriptVersion);
for (const { selector, gradeLevels, customGradeLevelsList, customGradeLevelsListChangeHandler, } of [
{
selector: "#qjh-menu-feat-courses",
gradeLevels: coursesGradeLevelsList,
customGradeLevelsList: customGradeLevels,
customGradeLevelsListChangeHandler: (value) => GM_setValue("qjh_customGradeLevels", value),
},
{
selector: "#qjh-menu-feat-self-courses",
gradeLevels: selfCoursesGradeLevelsList,
customGradeLevelsList: customSelfGradeLevels,
customGradeLevelsListChangeHandler: (value) => GM_setValue("qjh_customSelfGradeLevels", value),
},
]) {
const element = await waitForElementLoaded(selector);
if (gradeLevels === null) {
continue;
}
for (const gradeLevel of gradeLevels) {
const label = document.createElement("label");
label.className = "form-checkbox form-inline";
const input = document.createElement("input");
input.type = "checkbox";
input.checked =
customGradeLevelsList().indexOf(gradeLevel) !== -1;
input.onchange = () => {
if (input.checked) {
customGradeLevelsListChangeHandler(Array.of(...customGradeLevelsList(), gradeLevel));
}
else {
customGradeLevelsListChangeHandler(customGradeLevelsList().filter((it) => it !== gradeLevel));
}
};
const i = document.createElement("i");
i.className = "form-icon";
label.appendChild(input);
label.appendChild(i);
label.append(gradeLevel);
element.appendChild(label);
}
}
const closeButton = await waitForElementLoaded("#qjh-menu-close-button");
closeButton.onclick = () => {
menuElement.style.display = "none";
};
const toggleInputs = nodeListToArray(document.querySelectorAll("input")).filter((element) => element.getAttribute("qjh-type") === "toggle");
for (const toggleInput of toggleInputs) {
const key = toggleInput.getAttribute("qjh-key");
toggleInput.checked = GM_getValue(key);
toggleInput.onchange = () => {
GM_setValue(key, toggleInput.checked);
};
}
const featButtons = nodeListToArray(document.querySelectorAll("button")).filter((element) => element.getAttribute("qjh-feat-key") !== null);
for (const featButton of featButtons) {
const key = featButton.getAttribute("qjh-feat-key");
const feature = features.find((feature) => feature.key === key);
featButton.onclick = () => {
if (feature.enabled()) {
showMessage(`手动激活功能:${feature.title}`, "green");
feature.task();
}
else {
showMessage(`功能 ${feature.title} 未被启用!`, "red");
}
};
}
}
async function startCourse(courseId) {
const answers = await getCourseAnswers(courseId);
if (answers === null) {
showMessage(`[${courseId}] 无法获取当前课程的答案!`, "red");
return false;
}
else {
location.href = `https://www.2-class.com/courses/exams/${courseId}`;
}
}
async function taskCourses(isSelfCourses) {
if (!isLogined()) {
showMessage("你还没有登录!", "red");
return;
}
let gradeLevels = await (isSelfCourses
? selfCoursesGradeLevels
: coursesGradeLevels)();
if (gradeLevels === null) {
showMessage(`获取年级名列表失败,功能已中止!`, "red");
return;
}
console.debug("获取总年级名列表", gradeLevels);
gradeLevels = isSelfCourses ? customSelfGradeLevels() : customGradeLevels();
console.debug("已选择的年级列表", gradeLevels);
for (const gradeLevel of gradeLevels) {
const coursesList = isSelfCourses
? await getSelfCoursesByGradeLevel(gradeLevel)
: await getCoursesByGradeLevel(gradeLevel);
if (coursesList === null) {
showMessage(`[${gradeLevel}] 获取当前年级的课程列表失败,已跳过当前年级!`, "red");
}
const courseIds = coursesList
.filter((it) => !it.isFinish && it.title !== "期末考试")
.map((it) => it.courseId);
if (courseIds.length === 0) {
console.debug(`[${gradeLevel}] 所有${isSelfCourses ? "自学" : ""}课程都是完成状态,已跳过!`);
return;
}
console.debug(`[${gradeLevel}] 未完成的${isSelfCourses ? "自学" : ""}课程`, courseIds);
let committed = 0;
for (const courseId of courseIds) {
if (courseId === "finalExam") {
return;
}
if (!isNone(courseId)) {
const result = await startCourse(courseId);
if (result) {
committed++;
}
else {
console.error(`[${courseId}] 无法提交当前课程,已跳过!`);
}
}
else {
console.error(`[${gradeLevel}] 无法找到 courseId,已跳过!`);
}
}
showMessage(`成功完成了 ${committed} 个${isSelfCourses ? "自学" : ""}课程!`, "green");
}
}
async function taskSingleCourse() {
if (!isLogined()) {
showMessage("你还没有登录!", "red");
return;
}
const courseId = location.pathname.match(/(\d+)/g)[0];
const answers = await getCourseAnswers(courseId);
await emulateExamination(answers, "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > button", "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > div.exam-content-btnbox > button", "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > div.exam-content-btnbox > div > button:nth-child(2)", (answers, _) => {
const firstAnswer = answers.shift().toString();
return {
type: "index",
answer: firstAnswer,
matchedQuestion: null,
};
}, `答题 [${courseId}]`, answers.length, 50);
const passText = await waitForElementLoaded("#app > div > div.home-container > div > div > div > div.exam-box > div > div > p.exam-pass-title");
if (passText) {
const courses = [];
const courseLevels = customGradeLevels();
for (const courseLevel of courseLevels) {
const result = await getCoursesByGradeLevel(courseLevel);
for (const course of result) {
courses.push(course);
}
}
const courseIds = courses
.filter((it) => !it.isFinish && it.title !== "期末考试")
.map((it) => it.courseId);
if (courseIds.length === 0) {
showMessage("所有的课程已全部自动完成!", "green");
location.href = "https://www.2-class.com/courses/";
}
else {
location.href = `https://www.2-class.com/courses/exams/${courseIds[0]}`;
}
}
}
async function emulateExamination(answers, startButtonSelector, primaryNextButtonSelector, secondaryNextButtonSelector, answerHandler, examinationName, size = 100, interval = 3000, afterStart = async () => { }) {
let isExaminationStarted = false;
let count = 0;
const next = async (nextAnswers, nextButton = null) => {
const questionElement = await waitForElementLoaded(".exam-content-question");
const questionText = removeStuffs(questionElement.innerText.split("\n")[0]);
if (!isExaminationStarted) {
const primaryNextButton = await waitForElementLoaded(primaryNextButtonSelector);
isExaminationStarted = true;
await next(nextAnswers, primaryNextButton);
}
else {
let nextSecButton = nextButton;
if (count > 0) {
nextSecButton = await waitForElementLoaded(secondaryNextButtonSelector);
}
if (!isNone(size) && count < size) {
nextSecButton.onclick = async () => {
setTimeout(async () => await next(nextAnswers, nextSecButton), 0);
};
let { type, answer, matchedQuestion } = answerHandler(answers, questionText);
if (isNone(answer)) {
showMessage(`未找到此题的答案,请手动回答,或等待题库更新:${questionText}`, "red");
count++;
return;
}
else {
const selections = document.getElementsByClassName("exam-single-content-box");
console.debug("选择", answer, selections);
const finalQuestion = matchedQuestion || questionText;
if (!isFullAutomaticEmulationEnabled()) {
showMessage(`${finalQuestion ? finalQuestion + "\n" : ""}第 ${count + 1} 题答案:${type === "index" ? toDisplayAnswer(answer) : answer}`, "green");
}
if (type === "text") {
for (let answerText of answer.split("||")) {
answerText = removeStuffs(answerText);
const selectionElements = htmlCollectionToArray(selections).filter((it) => {
const match = it.innerText.match(/^([A-Z])([.。,,、.])(.*)/);
const answerContent = removeStuffs(match[1 + 2]);
return (!isNone(answerContent) &&
(answerContent === answerText ||
fuzzyMatch(answerContent, answerText).matched));
});
selectionElements.map((it) => it.click());
}
}
else {
for (const answerIndex of answer
.split(",")
.filter((it) => it !== "")
.map((it) => Number(it))) {
const selectionElement = selections[answerIndex];
selectionElement.click();
}
}
if (isFullAutomaticEmulationEnabled()) {
setTimeout(() => nextSecButton.click(), interval);
}
count++;
}
}
}
};
const startButton = await waitForElementLoaded(startButtonSelector);
if (isFullAutomaticEmulationEnabled()) {
showMessage(`自动开始 ${examinationName}!`, "blue");
startButton.click();
await afterStart();
next(answers, null);
}
else {
startButton.onclick = async () => {
showMessage(`开始 ${examinationName}!`, "blue");
await afterStart();
next(answers, null);
};
}
}
async function taskSkip() {
if (!isLogined()) {
showMessage("你还没有登录!", "red");
return;
}
const courseId = location.pathname.match(/(\d+)/g)[0];
const video = (await waitForElementLoaded("#app > div > div.home-container > div > div > div:nth-child(2) > div > div > div > div > div > video"));
const videoControlButton = await waitForElementLoaded("#app > div > div.home-container > div > div > div:nth-child(2) > div > div > div > div > div > .prism-controlbar > .prism-play-btn");
videoControlButton.onclick = () => {
const endTime = video.seekable.end(0);
video.currentTime = endTime;
};
}
async function taskGetCredit() {
if (!isLogined()) {
showMessage("你还没有登录!", "red");
return;
}
const length = 5;
const num = await addMedal();
if (num !== undefined) {
showMessage(`成功领取禁毒徽章 [${num}]!`, "green");
}
else if (num === null) {
showMessage("领取徽章失败!", "red");
}
else {
showMessage("无法领取徽章(可能已领取过),已跳过!", "yellow");
console.warn("无法领取徽章(可能已领取过),已跳过!");
}
const categories = [
{ name: "public_good", tag: "read" },
{ name: "ma_yun_recommend", tag: "labour" }, // the `ma_yun_recommend` has lots of sub-categorys
{ name: "ma_yun_recommend", tag: "movie" },
{ name: "ma_yun_recommend", tag: "music" },
{ name: "ma_yun_recommend", tag: "physicalEducation" },
{ name: "ma_yun_recommend", tag: "arts" },
{ name: "ma_yun_recommend", tag: "natural" },
{ name: "ma_yun_recommend", tag: "publicWelfareFoundation" },
{ name: "school_safe", tag: "safeVolunteer" },
];
let done = 0;
let failed = 0;
let liked = 0;
for (const category of categories) {
const data = {
categoryName: category.name,
pageNo: 1,
pageSize: 100,
reqtoken: reqtoken(),
tag: category.tag,
};
const resources = await getBeforeResourcesByCategoryName(data);
if (resources === null) {
console.error(`无法获取分类 ${category.name} 的资源,已跳过!`);
continue;
}
console.debug(`获取分类 ${category.name} 的资源`, resources);
for (const resource of resources) {
if (done >= length)
break;
const resourceId = resource.resourceId;
const resourceData = { resourceId, reqtoken: reqtoken() };
const result = await addPCPlayPV(resourceData);
if (result) {
console.debug(`成功完成资源 [${resourceId}]:${resource.title}`);
done++;
}
else {
console.error(`无法完成资源 ${resourceId},已跳过!`);
failed++;
}
const likeResult = await likePC(resourceData);
if (likeResult) {
console.debug(`成功点赞资源 [${resourceId}]!`);
liked++;
}
else {
console.error(`资源点赞失败 [${resourceId}],已跳过!`);
}
}
}
let beforeDone = done;
const checkSuccess = setInterval(() => {
if (done !== 0) {
if (done === beforeDone) {
showMessage(`成功完成 ${done}/${done + failed} 个资源,点赞 ${liked} 个!`, "green");
clearInterval(checkSuccess);
}
else {
beforeDone = done;
}
}
}, 500);
}
async function taskFinalExamination() {
const supportedFinal = libs.supportedFinal;
const gradeLevel = accountGradeLevel();
if (supportedFinal.hasOwnProperty(gradeLevel)) {
const paperName = supportedFinal[gradeLevel];
let papers = libs[paperName];
await emulateExamination(papers.map((it) => it.answer), "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > button", "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > div.exam-content-btnbox > button", "#app > div > div.home-container > div > div > div > div:nth-child(1) > div > div.exam-content-btnbox > div > button:nth-child(2)", (_, question) => {
const [answerList, n] = accurateFind(papers, question) ||
fuzzyFind(papers, question) || [[], 0];
return {
type: "text",
answer: n > 0 ? answerList.map((it) => it.answer).join("||") : null,
matchedQuestion: n > 0 ? answerList.map((it) => it.realQuestion).join("||") : null,
};
}, "期末考试", 10, // 一共 10 道题
3000 // 默认题目间隔 3s
);
}
else {
showMessage(`你的年级 [${gradeLevel}] 暂未支持期末考试!`, "red");
return;
}
}
async function taskMultiComplete() {
}
async function taskCompetition() {
const supportedCompetition = libs.supportedCompetition;
const gradeLevel = accountGradeLevel();
let gradeGroup;
const gradesPrimary = {
一年级: 1,
二年级: 2,
三年级: 3,
四年级: 4,
五年级: 5,
六年级: 6,
};
if (gradeLevel in gradesPrimary) {
gradeGroup = "小学组";
}
else {
gradeGroup = "中学组";
}
if (supportedCompetition.hasOwnProperty(gradeGroup)) {
showMessage(`已自动选择 [${gradeGroup}] 知识竞赛题库`, "cornflowerblue");
const paperName = supportedCompetition[gradeGroup];
const papers = libs[paperName];
if (!Array.isArray(papers)) {
showMessage(`[${gradeGroup}] 暂不支持知识竞赛!`, "red");
return;
}
await emulateExamination(papers.map((it) => it.answer), "#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.exam-box > div > div.exam_content_bottom_btn > button", "#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.exam-box > div.competition-sub > button", "#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.exam-box > div.competition-sub > button.ant-btn.ant-btn-primary", (_, question) => {
const [answerList, n] = accurateFind(papers, question) ||
fuzzyFind(papers, question) || [[], 0];
return {
type: "text",
answer: n > 0 ? answerList.map((it) => it.answer).join("||") : null,
matchedQuestion: n > 0 ? answerList.map((it) => it.realQuestion).join("||") : null,
};
}, "知识竞赛", 20, // 最大题目数,竞赛只有 20 道题目,如果未定义并打开了 `自动下一题并提交` 会导致循环提示最后一题 80 次
3000, // 与下一题的间隔时间,单位毫秒,默认 3 秒
async () => {
const gradeGroupDialog = await waitForElementLoaded("#app > div > div.home-container > div > div > div.competiotion-exam-box-all > div.dialog-mask > div");
const options = nodeListToArray(gradeGroupDialog.querySelectorAll(".option"));
const filteredOptions = options.filter((it) => it.innerHTML === gradeGroup);
const resultOption = filteredOptions[0];
if (filteredOptions.length < 1 || isNone(resultOption)) {
showMessage(`[${gradeGroup}] 暂不支持知识竞赛!`, "red");
return;
}
else {
resultOption.click();
}
});
}
else {
showMessage(`你的年级 [${gradeLevel}] 暂未支持知识竞赛!`, "red");
return;
}
}
function showMessage(text, color) {
Toastify({
text,
duration: toastifyDuration,
newWindow: true,
gravity: toastifyGravity,
position: toastifyPosition,
stopOnFocus: true,
style: { background: color },
}).showToast();
}
function featureNotAvailable(name = "(未知)") {
showMessage(`${name} 功能当前不可用,请尝试刷新页面。如果问题依旧请上报这个 bug!`, "red");
}
function isNone(obj) {
return obj == undefined || obj == null;
}
function getGMValue(name, defaultValue) {
let value = GM_getValue(name);
if (isNone(value)) {
value = defaultValue;
GM_setValue(name, defaultValue);
}
return value;
}
async function waitForElementLoaded(querySelector) {
return new Promise((resolve, reject) => {
let attempts = 0;
const tryFind = () => {
const element = document.querySelector(querySelector);
if (element) {
resolve(element);
}
else {
attempts++;
if (attempts >= 30) {
console.error(`无法找到元素 [${querySelector}],已放弃!`);
reject();
}
else {
setTimeout(tryFind, 250 * Math.pow(1.1, attempts));
}
}
};
tryFind();
});
}
function removeStuffs(string) {
return isNone(string)
? null
: string
.replace(/\s*/g, "")
.replace(/[,。?!;:—【】(),.?!;:-\[\]\(\)]/g, "");
}
function toDisplayAnswer(answer) {
const alphas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
let result = "";
for (const singleAnswer of answer.split(",")) {
const index = Number(singleAnswer);
result = result + alphas[index];
}
return result;
}
function nodeListToArray(nodeList) {
return Array.prototype.slice.call(nodeList);
}
function htmlCollectionToArray(htmlCollection) {
const result = [];
for (const element of htmlCollection)
result.push(element);
return result;
}
function arrayDiff(array1, array2) {
return array1.concat(array2).filter((v, _, array) => {
return array.indexOf(v) === array.lastIndexOf(v);
});
}
function fuzzyMatch(a, b) {
const aChars = a.split("");
const bChars = b.split("");
const length = aChars.length > bChars.length ? aChars.length : bChars.length;
const diff = arrayDiff(aChars, bChars);
const diffLength = diff.length;
const unconfidence = diffLength / length;
return {
matched: 1 - unconfidence >= fuzzyFindConfidenceTreshold,
confidence: 1 - unconfidence,
};
}
function accurateFind(papers, question) {
const results = papers.filter((it) => removeStuffs(it.question) === removeStuffs(question));
if (results.length > 0) {
console.debug(`精确匹配问题:${question} → ${question}`);
return [
results.map((it) => {
return { answer: it.answer, realQuestion: it.question };
}),
results.length,
];
}
else {
return null;
}
}
function fuzzyFind(papers, question) {
const chars = question.split("");
const length = chars.length;
const percentages = [];
for (const paper of papers) {
const { matched, confidence } = fuzzyMatch(question, paper.question);
if (matched) {
percentages.push({
question: paper.question,
answer: paper.answer,
confidence,
});
}
}
const theMostConfidents = percentages
.filter((it) => it.confidence > 0)
.sort((a, b) => a.confidence - b.confidence);
if (theMostConfidents.length <= 0) {
console.error(`模糊匹配未找到高度匹配的结果:${question}`);
return null;
}
console.debug(`模糊匹配问题:${question} → ${theMostConfidents
.map((it) => `(${it.confidence})${it.question}`)
.join("||")}`);
return [
theMostConfidents.map((it) => {
return { answer: it.answer, realQuestion: it.question };
}),
theMostConfidents.length,
];
}
async function insertValue(input, value) {
input.value = value;
const event = new Event("input", {
bubbles: true,
});
const tracker = input._valueTracker;
event.simulated = true;
if (tracker) {
tracker.setValue(value);
}
input.dispatchEvent(event);
}
async function login(account, password) {
const loginButton = await waitForElementLoaded("#app > div > div.home-container > div > div > main > div.white-bg-panel > div.login_home > div > div.padding-panel.btn-panel > div > button");
loginButton.click();
const accountInput = (await waitForElementLoaded("#account"));
const passwordInput = (await waitForElementLoaded("#password"));
passwordInput.type = "text";
const submitButton = await waitForElementLoaded("body > div:nth-child(14) > div > div.ant-modal-wrap > div > div.ant-modal-content > div > form > div > div > div > button");
await new Promise((resolve) => setTimeout(resolve, 500));
await insertValue(accountInput, account);
await insertValue(passwordInput, password);
submitButton.click();
waitForElementLoaded("#login_nc")
.then(async () => {
showMessage("正在进行模拟滑块验证,请稍等...", "green");
await mockVerify();
waitForElementLoaded("div > div > div > div.ant-notification-notice-description").then(() => {
showMessage("检测到滑块验证登入失败,请重新刷新网页并确保开发者工具处于开启状态!", "red");
});
})
.catch(() => {
console.log("无滑块验证出现,已直接登入");
});
}
async function mockVerify() {
const mockDistance = 394; // 滑块验证的长度
const mockInterval = 20; // 滑动间隔
const mockButtonId = "nc_1_n1z"; // 滑块验证的可交互按钮 ID
const verifyButton = document.getElementById(mockButtonId);
const clientRect = verifyButton.getBoundingClientRect();
const x = clientRect.x;
const y = clientRect.y;
const mousedown = new MouseEvent("mousedown", {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
});
verifyButton.dispatchEvent(mousedown);
let dx = 0;
let dy = 0;
const timer = setInterval(function () {
const _x = x + dx;
const _y = y + dy;
const mousemoveEvent = new MouseEvent("mousemove", {
bubbles: true,
cancelable: true,
clientX: _x,
clientY: _y,
});
verifyButton.dispatchEvent(mousemoveEvent);
if (_x - x >= mockDistance) {
clearInterval(timer);
const mouseupEvent = new MouseEvent("mouseup", {
bubbles: true,
cancelable: true,
clientX: _x,
clientY: _y,
});
verifyButton.dispatchEvent(mouseupEvent);
}
else {
dx += Math.ceil(Math.random() * 50);
}
}, mockInterval);
}
const container = document.createElement("div");
container.setAttribute("id", "qjh-menu");
container.innerHTML = `<style>
.qjh-menu {
height: max-content;
box-shadow: 1px 1px 10px #909090;
padding: 1em;
position: fixed;
z-index: 999;
right: 1%;
top: 3%;
width: 25%;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
}
.form-inline {
display: inline-block;
}
</style>
<div class="card container qjh-menu">
<div class="card-header">
<div class="card-title text-bold h5" id="qjh-menu-title">
QingJiaoHelper
<button
class="btn btn-link float-right"
type="button"
id="qjh-menu-close-button"
>
❌
</button>
</div>
</div>
<div class="card-body">
<div class="toast toast-warning">
⚠注意:勾选的功能会在下一次刷新页面时<mark><b>自动激活</b></mark
>,未勾选的功能只能手动启用!点击<b>一键完成</b>按钮可以在这个菜单中直接完成,而不用手动跳转到对应页面。
</div>
<div class="divider text-center" data-content="考试"></div>
<div class="form-group">
<label class="form-switch">
<b>期末考试</b>
<input
type="checkbox"
qjh-type="toggle"
qjh-key="qjh_isTaskFinalExaminationEnabled"
/>
<i class="form-icon"></i>
<button class="btn btn-sm mx-2" type="button">
<a href="/courses/exams/finalExam">点击跳转</a>
</button>
</label>
</div>
<div class="form-group">
<label class="form-switch">
<b>知识竞赛</b>
<input
type="checkbox"
qjh-type="toggle"
qjh-key="qjh_isTaskCompetitionEnabled"
/>
<i class="form-icon"></i>
<button class="btn btn-sm mx-2" type="button">
<a href="/competition">点击跳转</a>
</button>
</label>
</div>
<div class="divider text-center" data-content="课程"></div>
<div>
<div class="form-group" id="qjh-menu-feat-courses">
<label class="form-switch">
<b>完成所选年级的课程</b>
<input
type="checkbox"
qjh-type="toggle"
qjh-key="qjh_isTaskCoursesEnabled"
/>
<i class="form-icon"></i>
<button class="btn btn-sm mx-2" type="button" qjh-feat-key="courses">
一键完成👉
</button>
</label>
</div>
<div class="form-group" id="qjh-menu-feat-self-courses">
<label class="form-switch">
<b>完成所选年级的自学课程</b>
<input
type="checkbox"
qjh-type="toggle"
qjh-key="qjh_isTaskSelfCourseEnabled"
/>
<i class="form-icon"></i>
<button
class="btn btn-sm mx-2"
type="button"
qjh-feat-key="selfCourse"
>
一键完成👉
</button>
</label>
</div>
<div class="divider text-center" data-content="其他"></div>
<div class="form-group"></div>
<label class="form-switch">
<b>获取每日学分(点赞视频和领取徽章)</b>
<input
type="checkbox"
qjh-type="toggle"
qjh-key="qjh_isTaskGetCreditEnabled"
/>
<i class="form-icon"></i>
<button
class="btn btn-sm mx-2"
type="button"
onclick="taskGetCredit"
qjh-feat-key="credit"
>
一键完成👉
</button>
</label>
</div>
<div class="form-group">
<label class="form-switch">
<b>自动开始作答、下一题和提交</b>
<input
type="checkbox"
qjh-type="toggle"
qjh-key="qjh_isFullAutomaticEmulationEnabled"
/>
<i class="form-icon"></i>
</label>
</div>
</div>
<div class="divider"></div>
<div class="card-footer text-gray">
本脚本由 FoliageOwO 以
<b><a href="https://www.gnu.org/licenses/gpl-3.0.en.html">GPL-3.0</a></b>
开源许可在 GitHub 开源,脚本地址:<a
href="https://github.com/FoliageOwO/QingJiaoHelper"
target="_blank"
>GitHub</a
>、<a
href="https://greasyfork.org/zh-CN/scripts/452984-qingjiaohelper"
target="_blank"
>GreasyFork</a
>。
</div>
</div>
</div>
`;
container.style.display = "none";
document.body.appendChild(container);
function showMenu() {
container.style.display = "unset";
}