// ==UserScript== // @name 微伴助手 // @namespace http://tampermonkey.net/ // @version 0.2.1 // @description 基于 v0.1 稳定版,完整修复并集成了考试功能与格式正确的答案导出功能。 // @author Yi & 毫厘 // @match https://weiban.mycourse.cn/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @connect weiban.mycourse.cn // @connect safety.mycourse.cn // @license MIT // ==/UserScript== (function () { 'use strict'; // ======================================================================== // 变量定义与初始化 (来自 v0.1) // ======================================================================== let USER_PROJECT_ID; let countdownInterval = null; const hashParams = window.location.hash.substring(1); if (hashParams) { const paramsPart = hashParams.indexOf('?') > -1 ? hashParams.substring(hashParams.indexOf('?') + 1) : ''; if (paramsPart) { const urlParams = new URLSearchParams(paramsPart); USER_PROJECT_ID = urlParams.get('projectId'); } } const COURSE_TYPES = ['3', '2']; let singleButton, allButton, examButton, exportButton, logPanel, statusPanel; if (!USER_PROJECT_ID) { console.error("非课程页面,脚本将保持静默。"); return; } // ======================================================================== // 核心工具函数 (来自 v0.1 并适配) // ======================================================================== function getUserInfo() { const userData = localStorage.getItem('user'); if (!userData) { logMessage("错误:未找到用户信息,请确保已登录。", 'error'); return null; } try { const user = JSON.parse(userData); if (!user.token || !user.userId || !user.tenantCode) { logMessage("错误:用户信息不完整。", 'error'); return null; } return user; } catch (e) { logMessage("错误:解析用户信息失败。", 'error'); return null; } } function makeRequest(options) { const userInfo = getUserInfo(); if (!userInfo) return Promise.reject("无法获取用户信息"); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || "POST", url: options.url, headers: options.headers || { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "x-token": userInfo.token }, data: options.data, responseType: options.responseType || "json", onload: function (response) { if (response.status >= 200 && response.status < 300) { let responseData = response.response; if (options.responseType === 'text') { try { const text = response.responseText; responseData = JSON.parse(text.substring(text.indexOf('(') + 1, text.lastIndexOf(')'))); } catch (e) { resolve(response.responseText); return; } } if (responseData && responseData.code && responseData.code !== "0") { logMessage(`API错误: ${responseData.message || responseData.msg}`, 'error'); reject(responseData); } else { resolve(responseData); } } else { logMessage(`HTTP 错误: ${response.status}`, 'error'); reject(response); } }, onerror: function (response) { logMessage(`网络错误: ${response.statusText}`, 'error'); reject(response); } }); }); } function logMessage(message, type = 'info') { if (!logPanel) return; const entry = document.createElement('div'); const timestamp = new Date().toLocaleTimeString(); entry.className = 'log-entry'; const timeSpan = document.createElement('span'); timeSpan.className = 'log-timestamp'; timeSpan.textContent = `[${timestamp}]`; entry.appendChild(timeSpan); const messageSpan = document.createElement('span'); messageSpan.className = `log-message log-${type}`; switch (type) { case 'error': messageSpan.textContent = `错误: ${message}`; break; case 'success': messageSpan.textContent = `成功: ${message}`; break; case 'warning': messageSpan.textContent = `警告: ${message}`; break; case 'category-start': messageSpan.textContent = `--- ${message} ---`; break; case 'category-end': messageSpan.textContent = `--- ${message} ---`; entry.appendChild(messageSpan); logPanel.appendChild(entry); { const divider = document.createElement('div'); divider.className = 'log-divider'; logPanel.appendChild(divider); } logPanel.scrollTop = logPanel.scrollHeight; return; default: messageSpan.textContent = message; break; } entry.appendChild(messageSpan); logPanel.appendChild(entry); logPanel.scrollTop = logPanel.scrollHeight; } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function setAllButtonsDisabled(disabled) { [singleButton, allButton, examButton, exportButton].forEach(btn => { if (btn) btn.disabled = disabled; }); } // ======================================================================== // 课程学习相关功能 (来自 v0.1) // ======================================================================== async function fetchAllCategories(userInfo, userProjectId) { logMessage("正在获取所有课程分类...", 'info'); let allCategories = []; for (const type of COURSE_TYPES) { try { const response = await makeRequest({ url: `https://weiban.mycourse.cn/pharos/usercourse/listCategory.do?timestamp=${Date.now()}`, data: `tenantCode=${userInfo.tenantCode}&userId=${userInfo.userId}&userProjectId=${userProjectId}&chooseType=${type}` }); if (response?.data) { allCategories.push(...response.data.map(cat => ({ ...cat, chooseType: type }))); } } catch (e) { logMessage(`获取类型 [${type}] 分类失败`, 'error'); } } return allCategories; } async function attemptCompleteNextCourseInCategory(userInfo, userProjectId, categoryCode, categoryName, chooseType) { const listCourseUrl = `https://weiban.mycourse.cn/pharos/usercourse/listCourse.do?timestamp=${Date.now()}`; const listCourseData = `tenantCode=${userInfo.tenantCode}&userId=${userInfo.userId}&userProjectId=${userProjectId}&chooseType=${chooseType}&categoryCode=${categoryCode}`; const delayMilliseconds = (Math.random() * 10 + 15) * 1000; //核心模拟学习函数 try { const courseListResponse = await makeRequest({ url: listCourseUrl, data: listCourseData }); const targetCourse = courseListResponse.data?.find(c => c.finished !== 1); if (!targetCourse) return false; logMessage(`找到目标课程: ${targetCourse.resourceName}`, 'info'); await makeRequest({ url: `https://weiban.mycourse.cn/pharos/usercourse/study.do?timestamp=${Date.now()}`, data: `tenantCode=${userInfo.tenantCode}&userId=${userInfo.userId}&courseId=${targetCourse.resourceId}&userProjectId=${userProjectId}` }); updateStatus(`模拟学习: ${targetCourse.resourceName}`, Math.round(delayMilliseconds / 1000)); await sleep(delayMilliseconds); await makeRequest({ method: "GET", url: `https://weiban.mycourse.cn/pharos/usercourse/v2/${targetCourse.userCourseId}.do?callback=jQuery&userCourseId=${targetCourse.userCourseId}&tenantCode=${userInfo.tenantCode}&_=${Date.now()}`, responseType: 'text' }); await sleep(5000); for (let i = 0; i < 3; i++) { try { const verifyResponse = await makeRequest({ url: listCourseUrl, data: listCourseData }); const updatedCourse = verifyResponse.data.find(c => c.userCourseId === targetCourse.userCourseId); if (updatedCourse?.finished === 1) { logMessage(`课程 "${targetCourse.resourceName}" 已成功完成!`, 'success'); return true; } logMessage(`验证尝试 ${i + 1}/3: 状态为 ${updatedCourse?.finished ?? '未知'}`, 'warning'); } catch (e) { logMessage(`验证尝试 ${i + 1}/3 失败`, 'error'); } if (i < 2) await sleep(4000); } logMessage(`课程 "${targetCourse.resourceName}" 状态验证失败`, 'error'); return false; } catch (error) { logMessage(`处理课程时发生错误: ${error.message}`, 'error'); return false; } } async function automateSingleCategory() { await setAllButtonsDisabled(true); singleButton.textContent = '处理中...'; try { const userInfo = getUserInfo(); if (!userInfo) return; let categories = await fetchAllCategories(userInfo, USER_PROJECT_ID); const unfinishedCategory = categories.find(cat => cat.finishedNum < cat.totalNum); if (unfinishedCategory) { logMessage("开始处理下一个课程...", 'info'); await attemptCompleteNextCourseInCategory(userInfo, USER_PROJECT_ID, unfinishedCategory.categoryCode, unfinishedCategory.categoryName, unfinishedCategory.chooseType); } else { logMessage("所有课程均已完成。", "success"); } } finally { await setAllButtonsDisabled(false); singleButton.textContent = '完成下一个'; } } async function automateAllUnfinishedCategories() { await setAllButtonsDisabled(true); allButton.textContent = '处理中...'; logMessage("开始尝试完成所有未完成的课程...", 'info'); updateStatus("开始处理所有课程..."); try { const userInfo = getUserInfo(); if (!userInfo) return; let categories = await fetchAllCategories(userInfo, USER_PROJECT_ID); let unfinishedCategories = categories.filter(cat => cat.finishedNum < cat.totalNum); if (unfinishedCategories.length === 0) { logMessage("所有课程已完成。", 'success'); return; } logMessage(`发现 ${unfinishedCategories.length} 个分类有未完成的课程。`, 'info'); for (const category of unfinishedCategories) { logMessage(`处理分类: ${category.categoryName}`, 'category-start'); while (true) { const success = await attemptCompleteNextCourseInCategory(userInfo, USER_PROJECT_ID, category.categoryCode, category.categoryName, category.chooseType); if (!success) { logMessage(`分类 "${category.categoryName}" 已无课可刷或出错。`, 'info'); break; } await sleep(5000); } } logMessage("所有未完成课程处理完毕。", 'success'); } finally { await setAllButtonsDisabled(false); allButton.textContent = '完成所有课程'; updateStatus("课程处理完毕"); } } // ======================================================================== // 考试及导出功能 // ======================================================================== async function getProjectList() { return makeRequest({ url: `https://weiban.mycourse.cn/pharos/index/listMyProject.do`, data: `tenantCode=${getUserInfo().tenantCode}&userId=${getUserInfo().userId}&ended=2` }).then(res => res.data).catch(() => null); } async function getExams(userProjectId) { return makeRequest({ url: `https://weiban.mycourse.cn/pharos/exam/listPlan.do`, data: `tenantCode=${getUserInfo().tenantCode}&userId=${getUserInfo().userId}&userProjectId=${userProjectId}` }).then(res => res.data).catch(() => null); } async function getExamHistory(examPlanId, examType) { return makeRequest({ url: `https://weiban.mycourse.cn/pharos/exam/listHistory.do`, data: `tenantCode=${getUserInfo().tenantCode}&userId=${getUserInfo().userId}&examPlanId=${examPlanId}&examType=${examType}` }).then(res => res.data).catch(() => null); } async function getExamAnswer(userExamId) { return makeRequest({ url: `https://weiban.mycourse.cn/pharos/exam/reviewPaper.do`, data: `tenantCode=${getUserInfo().tenantCode}&userId=${getUserInfo().userId}&userExamId=${userExamId}&isRetake=2` }).then(res => res.data?.questions).catch(() => null); } async function startExam(userExamPlanId) { return makeRequest({ url: `https://weiban.mycourse.cn/pharos/exam/startPaper.do`, data: `tenantCode=${getUserInfo().tenantCode}&userId=${getUserInfo().userId}&userExamPlanId=${userExamPlanId}` }).then(res => res.data?.questionList).catch(() => null); } async function submitAnswer(examPlanId, userExamPlanId, questionId, answerIds) { return makeRequest({ url: `https://weiban.mycourse.cn/pharos/exam/recordQuestion.do`, data: `tenantCode=${getUserInfo().tenantCode}&userId=${getUserInfo().userId}&examPlanId=${examPlanId}&userExamPlanId=${userExamPlanId}&questionId=${questionId}&answerIds=${answerIds}&useTime=10` }).then(res => res.code === '0').catch(() => false); } async function submitPaper(userExamPlanId) { return makeRequest({ url: `https://weiban.mycourse.cn/pharos/exam/submitPaper.do`, data: `tenantCode=${getUserInfo().tenantCode}&userId=${getUserInfo().userId}&userExamPlanId=${userExamPlanId}` }).then(res => res.data).catch(() => null); } async function takeAllExams() { await setAllButtonsDisabled(true); examButton.textContent = '考试中...'; updateStatus("开始处理考试..."); try { const projects = await getProjectList(); if (!projects || projects.length === 0) { logMessage("未找到任何学习项目。", 'warning'); return; } for (const project of projects) { logMessage(`处理项目: "${project.projectName}"`, 'category-start'); const exams = await getExams(project.userProjectId); if (!exams || exams.length === 0) { logMessage("该项目下没有考试。", 'info'); continue; } // 筛选出已完成(有作答记录)的考试 const doneExams = exams.filter(x => x.examFinishNum > 0); let examAnswers = []; if (doneExams.length > 0) { logMessage(`发现 ${doneExams.length} 个已完成的考试,开始获取答案库...`, 'info'); for (const doneExam of doneExams) { const historyList = await getExamHistory(doneExam.examPlanId, doneExam.examType); if (!historyList) continue; for (const history of historyList) { const answers = await getExamAnswer(history.id); if (answers) { for (const answer of answers) { // 使用题目作为唯一标识,避免重复添加 if (!examAnswers.some(ea => ea.title === answer.title)) { examAnswers.push(answer); } } } } } if (examAnswers.length > 0) { logMessage(`成功构建答案库,共 ${examAnswers.length} 道题。`, 'success'); } else { logMessage(`未能从已完成的考试中提取到答案。`, 'warning'); } } // 如果没有任何考试记录,则进行一次“探索性”考试以生成答案库 if (examAnswers.length === 0) { logMessage("未发现可用答案库。将进行一次探索性考试以获取答案。", 'warning'); const firstExam = exams.find(e => e.examOddNum > 0); if (!firstExam) { logMessage("没有可用的考试来进行初次尝试。", "error"); continue; } logMessage(`将对《${firstExam.examPlanName}》进行初次尝试...`, 'info'); const questions = await startExam(firstExam.id); if (!questions) { logMessage("开始初次考试失败。这通常是因为需要人机验证。请手动进入该考试完成一次,然后再运行脚本。", 'error'); continue; // 继续处理下一个项目 } for (const q of questions) { // 统一选择第一个选项 await submitAnswer(firstExam.examPlanId, firstExam.id, q.id, q.optionList[0].id); } await submitPaper(firstExam.id); logMessage("初次尝试已提交。脚本将停止,请再次点击“开始考试”按钮,脚本将使用新生成的答案进行补考。", 'success'); return; // 关键:停止当前所有操作,让用户重新触发 } // 寻找需要进行且分数低于100的考试 const todoExam = exams.find(x => x.examOddNum > 0 && x.examScore < 100); if (!todoExam) { logMessage("所有考试均已完成或达标。", 'success'); logMessage(`项目: "${project.projectName}" 处理完毕`, 'category-end'); continue; } logMessage(`开始对《${todoExam.examPlanName}》进行正式考试...`, 'info'); const questions = await startExam(todoExam.id); if (!questions) { logMessage("开始正式考试失败。这通常是因为需要人机验证。请手动进入该考试完成一次,然后再运行脚本。", 'error'); continue; // 继续处理下一个项目 } let notFoundCount = 0; for (const q of questions) { const foundAnswer = examAnswers.find(a => a.title === q.title); let answerIds = []; if (foundAnswer) { // 从答案库中找到正确选项的内容 const correctOptionsContent = foundAnswer.optionList .filter(opt => opt.isCorrect === 1) .map(opt => opt.content); // 在当前考试的选项中,根据内容匹配,找到对应的ID answerIds = q.optionList .filter(currentOpt => correctOptionsContent.includes(currentOpt.content)) .map(currentOpt => currentOpt.id); } // 如果答案库中没有或者匹配失败,则默认选第一个 if (answerIds.length === 0) { notFoundCount++; answerIds.push(q.optionList[0].id); } await submitAnswer(todoExam.examPlanId, todoExam.id, q.id, answerIds.join(',')); } if (notFoundCount > 0) { logMessage(`本次有 ${notFoundCount} 道题未找到答案,已蒙第一个选项。`, 'warning'); } logMessage("所有题目回答完毕,等待3秒后交卷...", 'info'); await sleep(3000); const result = await submitPaper(todoExam.id); if (result) { logMessage(`交卷成功!本次得分: ${result.score}`, 'success'); if (result.score < 100) { logMessage("得分未满,若还有考试次数,可再次运行脚本刷分。", "info"); } } else { logMessage("交卷失败。", 'error'); } logMessage(`项目: "${project.projectName}" 处理完毕`, 'category-end'); } } finally { await setAllButtonsDisabled(false); examButton.textContent = '开始考试'; updateStatus("考试任务结束"); } } async function exportAnswersToDoc() { await setAllButtonsDisabled(true); exportButton.textContent = '导出中...'; updateStatus("开始构建答案库..."); logMessage("开始构建答案库以供导出...", "info"); try { const projects = await getProjectList(); if (!projects) { logMessage("未找到学习项目。", 'warning'); return; } let allExamAnswers = []; for (const project of projects) { const exams = await getExams(project.userProjectId); if (!exams) continue; const doneExams = exams.filter(x => x.examFinishNum > 0); if (doneExams.length === 0) continue; logMessage(`从项目 "${project.projectName}" 中提取答案...`, 'info'); for (const doneExam of doneExams) { const historyList = await getExamHistory(doneExam.examPlanId, doneExam.examType); if (!historyList) continue; for (const history of historyList) { const answers = await getExamAnswer(history.id); if (answers) { for (const answer of answers) { if (!allExamAnswers.some(ea => ea.title === answer.title)) { allExamAnswers.push(answer); } } } } } } if (allExamAnswers.length === 0) { logMessage("未能提取到任何答案。", "warning"); return; } logMessage(`答案库构建完成,共 ${allExamAnswers.length} 道题。开始生成Word...`, "success"); let contentHtml = `
导出时间: ${new Date().toLocaleString()}
${optionLetter}. ${opt.content.replace(//g, '>')}
`; }); contentHtml += `${index + 1}. ${q.title.replace(//g, '>')} [${q.typeLabel}]
正确答案:${correctAnswerLetters.join(', ')}