// ==UserScript== // @name 贵州继续教育自动刷题 PRO // @namespace https://gzjxjy.gzsrs.cn/ // @version 2.9.9-exam // @description 贵州继续教育自动考试,在线题库实时更新 // @author feng8848 // @match https://gzjxjy.gzsrs.cn/* // @match https://gzjxjy.gzsrs.cn/thr102-profession-tp/template/pc/* // @match https://gzjxjy.gzsrs.cn/thr102-profession-tp/template/pc/ // @grant GM_xmlhttpRequest // @connect wachtapi.rqzb.top // @connect api.qrserver.com // @run-at document-start // @antifeature membership // @noframes // @license All Rights Reserved // ==/UserScript== (function () { 'use strict'; const VERSION = '2.9.9-exam'; /* ======================== 授权 & 公众号配置 ======================== */ const GROUP_NUMBER = '1063839163'; // 官方交流群号 const PUBLIC_ACCOUNT = '陈风的思考日志'; // 微信公众号名称 // ---- 公众号二维码展示方案(三选一,优先级从上到下)---- // 方案A(推荐):将公众号二维码图片转为 Base64 后填入下方变量 // 获取方式:微信后台下载二维码 -> 在线工具转 Base64 -> 粘贴到这里 const QR_CODE_BASE64 = ''; // 例: 'data:image/png;base64,iVBORw0KGgo...' // 方案B:若已知公众号关注链接,脚本自动用在线API生成QR码 // 公众号关注链接格式通常为 https://weixin.qq.com/r/xxxxx const QR_FOLLOW_URL = 'http://weixin.qq.com/r/mp/9yC1rbHEbuwRrfXO93Xl'; // 方案C:若已有二维码图片的在线URL,直接填入 const QR_IMAGE_URL = ''; // 例: 'https://example.com/qrcode.png' // ---- 推荐码验证配置 ---- // 推荐码由微信公众号自动发放,后端 /api/verify-code 验证 const VERIFY_STORAGE_KEY = 'autoExamPro_verified'; const win = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; console.log(`%c[刷题PRO] 油猴脚本启动 v${VERSION} (document-start, exam-only)...`, 'color: #E91E63; font-size: 14px; font-weight: bold;'); function fakeActivity() { ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll'].forEach(t => { try { document.dispatchEvent(new Event(t, { bubbles: true })); } catch (e) {} }); } try { const _origTitle = document.title; document.title = '[刷题PRO已加载] ' + _origTitle; setTimeout(() => { document.title = _origTitle; }, 3000); const _wAdd = win.addEventListener.bind(win); const _dAdd = document.addEventListener.bind(document); const BLOCKED_EVENTS = ['blur', 'visibilitychange', 'webkitvisibilitychange', 'mozvisibilitychange', 'msvisibilitychange', 'pagehide', 'freeze']; win.addEventListener = function (t, l, o) { if (BLOCKED_EVENTS.includes(t)) return; return _wAdd(t, l, o); }; document.addEventListener = function (t, l, o) { if (BLOCKED_EVENTS.includes(t)) return; return _dAdd(t, l, o); }; ['hidden', 'mozHidden', 'msHidden', 'webkitHidden'].forEach(p => { Object.defineProperty(document, p, { get: () => false, configurable: true }); }); ['visibilityState', 'mozVisibilityState', 'msVisibilityState', 'webkitVisibilityState'].forEach(p => { Object.defineProperty(document, p, { get: () => 'visible', configurable: true }); }); document.hasFocus = () => true; } catch (antiCheatErr) { console.error('[刷题PRO] 反作弊注入失败:', antiCheatErr); } const CFG = { activityInterval: 25, showPanel: true, examChoiceDelay: 5000, examFillDelay: 10000, examRandomOffset: 2000, examAutoAccumulate: true, examMaxRetries: 3, examPassRate: 0.6, backendUrl: 'https://wachtapi.rqzb.top/yh', }; let isExamRunning = false; let isAutoRunning = false; let _examLoopActive = false; let _notLoggedIn = false; let statusText = '等待中...'; let logLines = []; let courseList = []; let activityTimer = null; function log(msg, color) { color = color || '#E91E63'; const time = new Date().toLocaleTimeString(); logLines.push(`[${time}] ${msg}`); if (logLines.length > 5000) logLines.splice(0, logLines.length - 5000); console.log(`%c[刷题PRO] ${msg}`, `color: ${color}; font-weight: bold;`); updateLogPanel(); } function escapeHtml(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } /* ======================== 推荐码验证(后端API) ======================== */ // 推荐码由微信公众号自动发放,后端 /api/verify-code 验证 function isVerified() { try { const data = JSON.parse(localStorage.getItem(VERIFY_STORAGE_KEY) || '{}'); if (!data.code) return false; return /^[A-Z0-9]{8}$/.test(data.code); } catch (e) { return false; } } function setVerified(code) { localStorage.setItem(VERIFY_STORAGE_KEY, JSON.stringify({ code: code.toUpperCase() })); } function getQrCodeSrc() { if (QR_CODE_BASE64) return QR_CODE_BASE64; if (QR_FOLLOW_URL) return `https://api.qrserver.com/v1/create-qr-code/?size=180x180&margin=8&data=${encodeURIComponent(QR_FOLLOW_URL)}`; if (QR_IMAGE_URL) return QR_IMAGE_URL; // 占位符 SVG — 请替换为真实公众号二维码 return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180' viewBox='0 0 180 180'%3E%3Crect width='180' height='180' fill='%23ffffff'/%3E%3Crect x='8' y='8' width='164' height='164' fill='none' stroke='%23E91E63' stroke-width='2' stroke-dasharray='6,4' rx='10'/%3E%3Ctext x='90' y='78' text-anchor='middle' font-size='14' fill='%23666' font-family='sans-serif'%3E%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BA%8C%E7%BB%B4%E7%A0%81%3C/text%3E%3Ctext x='90' y='102' text-anchor='middle' font-size='11' fill='%23999' font-family='sans-serif'%3E%E8%AF%B7%E6%9B%BF%E6%8D%A2%E4%B8%BA%E7%9C%9F%E5%AE%9E%E4%BA%8C%E7%BB%B4%E7%A0%81%3C/text%3E%3C/svg%3E"; } async function waitUntil(condFn, timeout, interval) { timeout = timeout || 30000; interval = interval || 1000; const start = Date.now(); while (Date.now() - start < timeout) { try { if (condFn()) return true; } catch (e) {} await sleep(interval); } return false; } function safeAsync(fn) { return () => { fn().catch(e => log('操作失败: ' + e.message, '#f44336')); }; } function getBase() { return win.Base; } function getApp() { return document.querySelector('#app')?.__vue__; } function findVM(name) { const app = getApp(); if (!app) return null; function find(vm, d) { if (!vm || d > 12) return null; if (vm.$options && vm.$options.name === name) return vm; for (const c of (vm.$children || [])) { const r = find(c, d + 1); if (r) return r; } return null; } return find(app.$root, 0); } function apiCall(url, data) { return new Promise((resolve, reject) => { if (_notLoggedIn) { reject(new Error('用户未登录')); return; } const Base = getBase(); if (!Base || !Base.sendRequest) { reject(new Error('Base 未就绪')); return; } Base.sendRequest(url, data, (res) => resolve(res), (err) => { const msg = (err && (err.message || err)) || ''; if (typeof msg === 'string' && msg.includes('未登录') && !_notLoggedIn) { _notLoggedIn = true; log('用户未登录,停止所有任务!请重新登录后重试', '#f44336'); stopAll(); } reject(err); }); }); } async function enrollCourse(course) { try { const res = await apiCall('/THRTTM015/save/THRTTM015_addTimetable', { bgze26: course.courseId, aae400: '01', }); log(`已报名: ${course.title}`, '#4CAF50'); return res; } catch (e) { log(`报名失败: ${course.title} - ${e.message}`, '#FF9800'); return null; } } async function fetchEnrolledCourses() { if (_notLoggedIn) return []; log('正在获取全部课程(公需科目+初任培训)...', '#2196F3'); try { let allCoursesRaw = []; const seenCourseIds = new Set(); const pubCourseIds = new Set(); function parseCourseResponse(res) { let list = []; if (res?.data?.data?.pageBean?.list) { list = res.data.data.pageBean.list; } else if (res?.data?.data?.result) { list = res.data.data.result; } else if (res?.data?.result) { list = res.data.result; } return list; } try { const resAll = await apiCall('/THRTTM012/query/THRTTM012_publicServiceQueryCase', { aae400: '01', bgze2t: '1', bgze27: '', bae735: '3', pageSize: 100, pageNo: 1, }); const pubList = parseCourseResponse(resAll); pubList.forEach(c => { if (!seenCourseIds.has(c.bgze26)) { seenCourseIds.add(c.bgze26); allCoursesRaw.push(c); } }); log(`公需科目返回 ${pubList.length} 门课程`, '#666'); } catch (e) { if (!_notLoggedIn) log(`获取公需科目课程失败: ${e.message}`, '#FF9800'); } try { const resTrain = await apiCall('/THRTTM012/query/THRTTM012_publicServiceQueryCase', { aae400: '01', bgze2t: '3', aae001: 2026, bgze2y: '1', timeFlag: '1', isFlag: '1', pageSize: '100', pageNumber: '1', }); const trainList = parseCourseResponse(resTrain); const newCount = trainList.filter(c => !seenCourseIds.has(c.bgze26)).length; trainList.forEach(c => { if (!seenCourseIds.has(c.bgze26)) { seenCourseIds.add(c.bgze26); allCoursesRaw.push(c); } }); log(`初任培训返回 ${trainList.length} 门课程(新增 ${newCount})`, '#666'); } catch (e) { if (!_notLoggedIn) log(`获取初任培训课程失败: ${e.message}`, '#FF9800'); } let progressMap = new Map(); async function refreshProgress() { progressMap = new Map(); try { const res = await apiCall('/THRTTM015/query/THRTTM015_queryMyTimetable', { aae400: '01', aae100: '1' }); if (res.serviceSuccess && res.data) { const enrolled = res.data.data ? (res.data.data.result || []) : (res.data.result || []); enrolled.forEach(c => progressMap.set(c.bgze26, c)); } } catch (e) { if (!_notLoggedIn) log(`获取进度数据失败: ${e.message}`, '#FF9800'); } } await refreshProgress(); log(`已报名 ${progressMap.size} 门`, '#666'); if (allCoursesRaw.length > 0) { const toEnroll = allCoursesRaw.filter(c => !progressMap.has(c.bgze26) && pubCourseIds.has(c.bgze26)); if (toEnroll.length > 0) { log(`自动报名 ${toEnroll.length} 门课程...`, '#FF9800'); for (const c of toEnroll) { await enrollCourse({ courseId: c.bgze26, title: c.bgze27 }); await sleep(500); } await sleep(1000); await refreshProgress(); log(`报名后已有 ${progressMap.size} 门课程`, '#4CAF50'); } } if (allCoursesRaw.length > 0) { const seen = new Set(); courseList = allCoursesRaw.filter(c => { if (seen.has(c.bgze26)) return false; seen.add(c.bgze26); return true; }).map(c => { const enrolled = progressMap.get(c.bgze26); return { courseId: c.bgze26, title: c.bgze27, themeId: enrolled ? enrolled.bgce90 : '', classId: enrolled ? (enrolled.bgceb0 || '') : '', progress: enrolled ? (enrolled.bgze3j || 0) : 0, creditHours: c.bgze2v || 0, enrolled: !!enrolled, isPub: pubCourseIds.has(c.bgze26), raw: enrolled || c }; }); const excluded = []; progressMap.forEach((c, id) => { if (!seen.has(id)) { excluded.push(c.bgze27 || id); } }); if (excluded.length > 0) { log(`过滤 ${excluded.length} 门非目标课程(专业科目等): ${excluded.join(', ')}`, '#888'); } log(`共 ${courseList.length} 门课程(已报名 ${progressMap.size} 门)`, '#4CAF50'); return courseList; } if (progressMap.size > 0) { courseList = []; progressMap.forEach(c => { courseList.push({ courseId: c.bgze26, title: c.bgze27, themeId: c.bgce90 || '', classId: c.bgceb0 || '', progress: c.bgze3j || 0, creditHours: c.bgze2v || 0, enrolled: true, raw: c }); }); log(`降级模式: 仅获取到 ${courseList.length} 门已报名课程`, '#FF9800'); return courseList; } if (!_notLoggedIn) log('课程列表为空', '#FF9800'); return []; } catch (e) { if (!_notLoggedIn) log('获取课程列表失败: ' + e.message, '#f44336'); return []; } } function goUserCenter() { const app = getApp(); if (app?.$root?.$router) { app.$root.$router.push({ name: 'userCenter' }); log('导航到个人中心', '#2196F3'); } } function stopAll() { isAutoRunning = false; isExamRunning = false; _examLoopActive = false; log('已停止', '#f44336'); updateUI(); } function apiRequest(method, path, body) { return new Promise((resolve, reject) => { const opts = { method: method, url: CFG.backendUrl + path, headers: { 'Content-Type': 'application/json' }, timeout: 15000, onload: (resp) => { try { if (!resp.responseText || resp.responseText.trim() === '') { reject(new Error(`后端返回空响应(HTTP ${resp.status})`)); return; } const data = JSON.parse(resp.responseText); if (data.success) { resolve(data.data); } else { reject(new Error(data.error || 'API返回失败')); } } catch (e) { reject(new Error(`响应解析失败(HTTP ${resp.status}): ${e.message}`)); } }, onerror: (err) => reject(new Error('网络请求失败: ' + (err.error || 'unknown'))), ontimeout: () => reject(new Error('请求超时')), }; if (body) { opts.data = JSON.stringify(body); } try { GM_xmlhttpRequest(opts); } catch (e) { const fetchOpts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) { fetchOpts.body = JSON.stringify(body); } fetch(CFG.backendUrl + path, fetchOpts) .then(r => r.json()) .then(data => { if (data.success) resolve(data.data); else reject(new Error(data.error || 'API返回失败')); }) .catch(e => reject(new Error('fetch回退也失败: ' + e.message))); } }); } const ExamDB = { async addQuestion(q) { return apiRequest('POST', '/api/questions', { courseId: q.courseId || '', questionText: q.questionText || '', questionType: q.questionType || 'choice', answer: q.answer || '', source: q.source || 'import', }); }, async searchQuestion(queryText, courseId) { const params = new URLSearchParams({ query: queryText }); if (courseId) params.set('courseId', courseId); const data = await apiRequest('GET', '/api/questions/search?' + params.toString()); return Array.isArray(data) ? data : []; }, async addExamHistory(record) { return apiRequest('POST', '/api/exam-history', record); } }; async function navigateToExam(course) { log(`正在进入考试: ${course.title}`, '#2196F3'); statusText = `考试: ${course.title}`; sessionStorage.setItem('courseItem', JSON.stringify({ ...(course.raw || {}), bgze26: course.courseId, bgze27: course.title, bgce90: course.themeId || '', bgceb0: course.classId || '', bgze3j: course.progress || 0, })); const app = getApp(); if (app?.$root?.$router) { app.$root.$router.push({ name: 'userCenter' }); await sleep(2000); app.$root.$router.push({ name: 'queryServices' }); } const qsLoaded = await waitUntil(() => !!findVM('queryServices'), 15000, 1000); if (!qsLoaded) { log('考试页面未加载', '#FF9800'); return false; } await sleep(2000); const btnFound = await waitUntil(() => { const btns = document.querySelectorAll('.btn.btn-o.btn-o-a'); for (const btn of btns) { if (btn.textContent.trim().includes('开始考试') && btn.offsetParent !== null) { return true; } } return false; }, 5000, 1000); if (!btnFound) { log('未找到"开始考试"按钮(可能已通过考试)', '#FF9800'); return false; } const examBtns = document.querySelectorAll('.btn.btn-o.btn-o-a'); for (const btn of examBtns) { if (btn.textContent.trim().includes('开始考试') && btn.offsetParent !== null) { btn.click(); break; } } const ewLoaded = await waitUntil(() => !!findVM('economizeWork'), 15000, 1000); if (!ewLoaded) { log('考试确认页未加载', '#FF9800'); return false; } await sleep(1000); const ew = findVM('economizeWork'); if (!ew) { log('economizeWork 组件未找到', '#f44336'); return false; } if (ew.$set) ew.$set(ew, 'selected', true); else ew.selected = true; const consentDivs = ew.$el?.querySelectorAll('div') || []; consentDivs.forEach(d => { if (d.textContent.includes('我已知晓并同意') && d.textContent.trim().length < 30) { d.click(); } }); await sleep(500); if (typeof ew.toExam === 'function') { ew.toExam(); } else { log('toExam 方法不存在', '#f44336'); return false; } const espLoaded = await waitUntil(() => !!findVM('EconomizeSearchPage'), 15000, 1000); if (!espLoaded) { log('考试答题页未加载', '#FF9800'); return false; } await sleep(2000); log('考试页面就绪', '#4CAF50'); return true; } function getExamVM() { return findVM('EconomizeSearchPage'); } function getQuestionType(question, esp, index) { if (esp && esp.countArr && esp.countArr.length > 0) { const typeMap = { '1': 'choice', '2': 'multichoice', '3': 'judge', '4': 'fill', '5': 'subjective' }; const typeOrder = []; for (const item of esp.countArr) { const count = parseInt(item.count) || 0; const type = typeMap[item.value] || 'choice'; for (let j = 0; j < count; j++) typeOrder.push(type); } const idx = (index != null) ? index : (esp.currentIndex || 0); if (idx < typeOrder.length) return typeOrder[idx]; } const optCount = question.gze6PoList?.length || 0; if (optCount === 2) { const labels = question.gze6PoList.map(o => (o.label || '').toLowerCase()); if (labels.some(l => l.includes('是')) && labels.some(l => l.includes('否'))) return 'judge'; } if (optCount <= 1) return 'fill'; if (question.bgze3p === '2' && optCount >= 4) return 'multichoice'; return 'choice'; } function qTypeLabel(qType) { const map = { judge: '判断', multichoice: '多选', fill: '填空', subjective: '主观', choice: '单选' }; return map[qType] || '单选'; } function answerLettersToText(answerLetters, opts) { if (!answerLetters || !opts || !opts.length) return answerLetters || ''; const letters = answerLetters.split(''); const texts = []; for (const l of letters) { const opt = opts.find(o => o.value === l); if (opt) texts.push(opt.bgze42 || opt.label || l); } return texts.join(','); } async function findAnswer(question) { const text = question.bgze3r || ''; if (!text) return null; try { const results = await ExamDB.searchQuestion(text, question.bgze26); if (results.length > 0 && results[0]._matchPct >= 60) { const stored = results[0].answer; const opts = question.gze6PoList || []; let answer = ''; if (stored && opts.length > 0 && !/^[A-Z]+$/.test(stored)) { const matchedLetters = []; for (const opt of opts) { const optText = opt.bgze42 || opt.label || ''; if (optText && stored.includes(optText)) { matchedLetters.push(opt.value); } } if (matchedLetters.length > 0) { answer = matchedLetters.sort().join(''); } } if (!answer && stored) { if (stored === question.bgze43 || /^[A-Z]+$/.test(stored)) { answer = stored; } } if (answer) { log(`题库匹配(${results[0]._matchPct.toFixed(0)}%): ${text.substring(0, 40)}...`, '#666'); return { answer, matched: true, matchPct: results[0]._matchPct }; } } } catch (e) { log(`搜索题库异常: ${e.message}`, '#FF9800'); } return null; } async function answerQuestion(esp, index) { const q = esp.drawerList[index]; if (!q) { log(`题目[${index}]不存在`, '#f44336'); return null; } const qType = getQuestionType(q, esp, index); const qText = (q.bgze3r || '').substring(0, 50); log(`[${index + 1}/${esp.drawerList.length}] ${qTypeLabel(qType)}: ${qText}...`, '#2196F3'); const result = await findAnswer(q); let answer = result?.answer || ''; if (!answer) { log('未找到答案,随机选择', '#FF9800'); const opts = q.gze6PoList || []; if (opts.length > 0) { if (qType === 'multichoice') { const pickCount = Math.min(2 + Math.floor(Math.random() * (opts.length - 1)), opts.length); const shuffled = opts.map(o => o.value).sort(() => Math.random() - 0.5); answer = shuffled.slice(0, pickCount).sort().join(''); } else { answer = opts[Math.floor(Math.random() * opts.length)].value; } } } if (qType === 'multichoice' && answer.length > 1) { answer = answer.split('').sort().join(''); } esp.drawerList[index].answer = (qType === 'multichoice') ? answer.split('') : answer; esp.currentIndex = index; if (typeof esp.addPersonalExam === 'function') { esp.addPersonalExam(); await sleep(1000); } const rawCorrect = esp.drawerList[index].bgze43; const correctAnswer = Array.isArray(rawCorrect) ? rawCorrect.sort().join('') : (rawCorrect || ''); const isCorrect = correctAnswer && answer === correctAnswer; log(`答案: ${answer}${correctAnswer ? ' | 正确: ' + correctAnswer : ''}${isCorrect ? ' ✓' : ' ✗'}`, isCorrect ? '#4CAF50' : '#FF9800'); return { question: q, answer, correctAnswer, matched: result?.matched, isCorrect, qType }; } async function accumulateQuestion(q, correctAnswer, esp, index) { if (!CFG.examAutoAccumulate || !correctAnswer) return; try { const existing = await ExamDB.searchQuestion(q.bgze3r || '', q.bgze26); if (existing.length > 0 && existing[0]._matchPct >= 80) return; const answerText = answerLettersToText(correctAnswer, q.gze6PoList || []); await ExamDB.addQuestion({ courseId: q.bgze26 || '', questionText: q.bgze3r || '', questionType: getQuestionType(q, esp, index), answer: answerText, source: 'exam_auto', importDate: new Date().toISOString() }); } catch (e) { log(`题库积累失败: ${e.message}`, '#FF9800'); } } async function runExamForCourse(course, retries) { retries = retries || 0; if (isExamRunning) { log('考试已在运行中', '#FF9800'); return false; } isExamRunning = true; const attemptLabel = retries > 0 ? ` (第${retries + 1}次)` : ''; log(`========== 开始考试: ${course.title}${attemptLabel} ==========`, '#E91E63'); try { const navOk = await navigateToExam(course); if (!navOk) { isExamRunning = false; return false; } const esp = getExamVM(); if (!esp || !esp.drawerList || esp.drawerList.length === 0) { log('考试题目未加载', '#FF9800'); isExamRunning = false; return false; } const totalQ = esp.drawerList.length; log(`共 ${totalQ} 道题目`, '#2196F3'); statusText = `答题中 0/${totalQ}`; const examDetails = []; let correctCount = 0; for (let i = 0; i < totalQ && isExamRunning; i++) { statusText = `答题中 ${i + 1}/${totalQ}`; updateUI(); const q = esp.drawerList[i]; const qType = getQuestionType(q, esp, i); const preDelay = (qType === 'fill' ? CFG.examFillDelay : CFG.examChoiceDelay) + Math.floor(Math.random() * CFG.examRandomOffset); await sleep(preDelay); if (!isExamRunning) break; const result = await answerQuestion(esp, i); if (result) { examDetails.push({ questionText: result.question.bgze3r, userAnswer: result.answer, correctAnswer: result.correctAnswer, matched: result.matched, isCorrect: result.isCorrect }); if (result.isCorrect) correctCount++; await accumulateQuestion(result.question, result.correctAnswer, esp, i); } const postDelay = (qType === 'fill' ? CFG.examFillDelay : CFG.examChoiceDelay) + Math.floor(Math.random() * CFG.examRandomOffset); await sleep(postDelay); } await sleep(3000); if (esp.visible) { log(`考试完成! ${correctCount}/${totalQ} 正确 (${(correctCount / totalQ * 100).toFixed(0)}%)`, correctCount / totalQ >= CFG.examPassRate ? '#4CAF50' : '#FF9800'); statusText = `考试: ${correctCount}/${totalQ}`; try { await ExamDB.addExamHistory({ courseId: course.courseId, courseTitle: course.title, examDate: new Date().toISOString(), totalQuestions: totalQ, correctCount, score: (correctCount / totalQ * 100).toFixed(0), passed: correctCount / totalQ >= CFG.examPassRate, details: examDetails }); } catch (e) { log(`考试历史记录失败: ${e.message}`, '#FF9800'); } if (typeof esp.confirmHandle === 'function') { esp.confirmHandle(); await sleep(2000); } } else { log('考试提交中...', '#2196F3'); statusText = '提交试卷...'; if (typeof esp.addPersonalExam === 'function') { esp.currentIndex = totalQ - 1; esp.addPersonalExam(); await sleep(3000); } if (esp.visible && typeof esp.confirmHandle === 'function') { esp.confirmHandle(); await sleep(2000); } } isExamRunning = false; const passRate = correctCount / totalQ; if (passRate >= CFG.examPassRate) { log(`考试通过! ${correctCount}/${totalQ} (${(passRate * 100).toFixed(0)}%)`, '#4CAF50'); return true; } if (retries < CFG.examMaxRetries) { log(`考试未通过 (${(passRate * 100).toFixed(0)}%),5秒后第${retries + 2}次重试...`, '#FF9800'); statusText = `重试 ${retries + 2}/${CFG.examMaxRetries + 1}...`; updateUI(); await sleep(5000); return runExamForCourse(course, retries + 1); } log(`考试未通过,已达最大重试次数 (${CFG.examMaxRetries + 1}次)`, '#f44336'); statusText = `未通过 ${correctCount}/${totalQ}`; return false; } catch (err) { log(`考试出错: ${err.message}`, '#f44336'); isExamRunning = false; return false; } } async function runPendingExams() { log('---------- 检查待考试课程 ----------', '#E91E63'); await fetchEnrolledCourses(); if (_notLoggedIn) { _examLoopActive = false; return 0; } const completed = courseList.filter(c => Number(c.progress || 0) >= 100); if (completed.length === 0) { log('没有已完成的课程需要考试', '#666'); return 0; } log(`发现 ${completed.length} 门已完成课程,逐一检查考试...`, '#E91E63'); let examCount = 0; _examLoopActive = true; try { for (const course of completed) { if (!_examLoopActive) break; statusText = `考试: ${course.title}`; updateUI(); const result = await runExamForCourse(course); if (result) { examCount++; log(`考试通过: ${course.title}`, '#E91E63'); } else { log(`考试跳过或未通过: ${course.title}`, '#666'); } await sleep(2000); } } finally { _examLoopActive = false; } log(`考试批次完成: ${examCount}/${completed.length} 门通过`, '#E91E63'); return examCount; } async function autoExamAll() { if (isExamRunning || isAutoRunning) { log('已有自动化任务在运行', '#FF9800'); return; } _notLoggedIn = false; log('========== 自动考试模式 ==========', '#E91E63'); await runPendingExams(); log('自动考试流程结束', '#E91E63'); statusText = '考试流程结束'; updateUI(); } /* ======================== 授权验证弹窗 ======================== */ function createVerifyModal(onSuccess) { const old = document.getElementById('ac-verify-overlay'); if (old) old.remove(); const qrSrc = getQrCodeSrc(); const overlay = document.createElement('div'); overlay.id = 'ac-verify-overlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.92);z-index:9999999;display:flex;align-items:center;justify-content:center;font-family:"Microsoft YaHei",sans-serif;'; overlay.innerHTML = `
自动刷题 PRO 授权验证
关注公众号获取推荐码后解锁使用
公众号二维码
公众号: ${PUBLIC_ACCOUNT}
扫码关注公众号 → 回复「推荐码」获取

官方交流群: ${GROUP_NUMBER}
遇到问题可加群反馈
`; document.body.appendChild(overlay); const input = document.getElementById('ac-verify-input'); const btn = document.getElementById('ac-verify-btn'); const errEl = document.getElementById('ac-verify-error'); async function doVerify() { const code = input.value.trim(); if (!code) { errEl.textContent = '请输入推荐码'; return; } btn.disabled = true; btn.textContent = '验证中...'; errEl.style.color = ''; errEl.textContent = ''; try { await apiRequest('POST', '/api/verify-code', { code }); setVerified(code); errEl.style.color = '#4CAF50'; errEl.textContent = '验证成功!正在解锁...'; setTimeout(() => { overlay.remove(); if (typeof onSuccess === 'function') onSuccess(); }, 800); } catch (e) { errEl.style.color = '#f44336'; errEl.textContent = (e && e.message) || '推荐码无效,请关注公众号获取正确推荐码'; input.style.borderColor = '#f44336'; setTimeout(() => { input.style.borderColor = 'rgba(255,255,255,0.15)'; }, 1500); } finally { btn.disabled = false; btn.textContent = '验证解锁'; } } btn.onclick = doVerify; input.onkeydown = (e) => { if (e.key === 'Enter') doVerify(); }; input.focus(); } function createPanel() { if (!CFG.showPanel) return; const old = document.getElementById('ac-exam-panel'); if (old) old.remove(); const el = document.createElement('div'); el.id = 'ac-exam-panel'; el.style.cssText = 'position:fixed;top:10px;right:10px;z-index:999999;background:rgba(20,20,30,0.95);color:#fff;padding:0;border-radius:14px;font-family:"Microsoft YaHei",sans-serif;font-size:13px;width:370px;box-shadow:0 8px 32px rgba(0,0,0,0.5);overflow:hidden;border:1px solid rgba(255,255,255,0.1);'; el.innerHTML = `
自动刷题 PRO v${VERSION}
题库检索 | 模拟答题 | 自动积累
状态: 等待中
课程: 检测中...
考试: 题库检索->阅读等待->答题->确认等待->自动积累
题库仅支持新增和查询,考试中自动积累正确答案
官方群: ${GROUP_NUMBER}  |  公众号: ${PUBLIC_ACCOUNT}
`; document.body.appendChild(el); document.getElementById('ac-exam-start').onclick = safeAsync(autoExamAll); document.getElementById('ac-exam-stop').onclick = () => stopAll(); document.getElementById('ac-exam-usercenter').onclick = () => goUserCenter(); document.getElementById('ac-exam-enrolled').onclick = safeAsync(async () => { _notLoggedIn = false; const list = await fetchEnrolledCourses(); const completed = list.filter(c => Number(c.progress || 0) >= 100); log(`共 ${list.length} 门课程,已完成 ${completed.length} 门可考试`, '#3F51B5'); completed.forEach((c, i) => log(` ${i + 1}. ${c.title} (${c.progress}%)`, '#666')); updateUI(); }); document.getElementById('ac-exam-log').onclick = () => { const box = document.getElementById('ac-exam-logbox'); box.style.display = box.style.display === 'none' ? 'block' : 'none'; updateLogPanel(); }; document.getElementById('ac-exam-exportlog').onclick = () => exportLogs(); el.querySelectorAll('button').forEach(btn => { btn.onmouseenter = () => { btn.style.opacity = '0.85'; }; btn.onmouseleave = () => { btn.style.opacity = '1'; }; }); let ox, oy, dragging = false; const header = document.getElementById('ac-exam-header'); header.onmousedown = (e) => { if (e.target.tagName === 'BUTTON') return; dragging = true; ox = e.clientX - el.getBoundingClientRect().left; oy = e.clientY - el.getBoundingClientRect().top; }; document.addEventListener('mousemove', (e) => { if (!dragging) return; el.style.left = (e.clientX - ox) + 'px'; el.style.top = (e.clientY - oy) + 'px'; el.style.right = 'auto'; }); document.addEventListener('mouseup', () => { dragging = false; }); log('面板已创建', '#E91E63'); } function updateUI() { const s = document.getElementById('ac-exam-s'); const c = document.getElementById('ac-exam-c'); if (s) { const st = isExamRunning ? '考试中' : (_examLoopActive ? '考试批次中' : '已就绪'); const col = isExamRunning ? '#E91E63' : (_examLoopActive ? '#FF9800' : '#FFC107'); s.innerHTML = `状态: ${st}`; } if (c) { if (courseList.length > 0) { const completed = courseList.filter(c => Number(c.progress || 0) >= 100).length; c.textContent = `课程: ${courseList.length}门 (可考试${completed}门) | ${statusText}`; } else { c.textContent = `课程: 未检测到 (请先进入课程页面) | ${statusText}`; } } } function updateLogPanel() { const box = document.getElementById('ac-exam-logbox'); if (box && box.style.display !== 'none') { box.innerHTML = logLines.slice(-40).map(escapeHtml).join('
') || '暂无日志'; box.scrollTop = box.scrollHeight; } } function exportLogs() { const text = logLines.join('\n') || '暂无日志'; const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `acexam_log_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.txt`; a.click(); URL.revokeObjectURL(url); log(`日志已导出: ${logLines.length} 条`, '#4CAF50'); } function initAfterReady() { console.log('%c[刷题PRO] DOM 就绪,初始化面板...', 'color: #E91E63;'); if (!activityTimer) { activityTimer = setInterval(fakeActivity, CFG.activityInterval * 1000); } createPanel(); // 授权验证:未验证则弹出验证弹窗并禁用面板按钮 if (!isVerified()) { const btns = document.querySelectorAll('#ac-exam-panel button'); btns.forEach(btn => { btn.disabled = true; btn.style.opacity = '0.35'; btn.style.cursor = 'not-allowed'; }); log('请先完成授权验证', '#FF9800'); createVerifyModal(() => { // 验证成功后恢复按钮 btns.forEach(btn => { btn.disabled = false; btn.style.opacity = ''; btn.style.cursor = ''; }); log('授权验证成功,已解锁全部功能', '#4CAF50'); updateUI(); }); } else { log('授权验证已通过', '#4CAF50'); } setInterval(updateUI, 2000); win.addEventListener('online', () => log('网络已恢复', '#4CAF50')); win.addEventListener('offline', () => log('网络已断开', '#f44336')); setTimeout(async () => { if (!isVerified()) return; const list = await fetchEnrolledCourses(); if (list.length > 0) { const completed = list.filter(c => Number(c.progress || 0) >= 100).length; log(`检测到 ${list.length} 门课程,${completed} 门已完成可考试,点击"自动考试"开始`, '#2196F3'); } updateUI(); }, 3000); win.AutoExamPro = { autoExamAll, stopAll, fetchEnrolledCourses, goUserCenter, enrollCourse, runExamForCourse, runPendingExams, getExamVM, cfg: CFG, get examDB() { return ExamDB; }, get courses() { return courseList; }, exportLogs, log, isVerified }; console.log(`%c[刷题PRO] v${VERSION} 初始化完成!`, 'color: #E91E63; font-size: 14px; font-weight: bold;'); console.log('%cAPI: AutoExamPro.autoExamAll() | .runExamForCourse() | .examDB.addQuestion() | .examDB.searchQuestion()', 'color: #9C27B0;'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAfterReady); } else { initAfterReady(); } })();