// ==UserScript== // @name 木金网课助手强辅版 // @namespace https://wk.bobo91.com/mujin-strong-helper // @version 3.3.18 // @description 木金网课助手强辅版:课程队列、考试入口队列、进度监控与异常防卡辅助工具,配合木金网课助手/OCS网课助手使用,互不干扰。 // @author 木金 // @match *://chaoxing.com/* // @match *://*.chaoxing.com/* // @match *://i.chaoxing.com/* // @match *://i.mooc.chaoxing.com/* // @match *://passport2.chaoxing.com/* // @match *://passport2-api.chaoxing.com/* // @match *://mooc1.chaoxing.com/* // @match *://mooc2-ans.chaoxing.com/* // @match *://fanya.chaoxing.com/* // @match *://*.fanya.chaoxing.com/* // @match *://*.jxjy.chaoxing.com/* // @match *://chaoxing.com.cn/* // @match *://*.chaoxing.com.cn/* // @match *://xueyinonline.com/* // @match *://*.xueyinonline.com/* // @match *://*.zhihuishu.com/* // @match *://*.icve.com.cn/* // @match *://*.icourse163.org/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_notification // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect chaoxing.com // @connect *.chaoxing.com // @connect passport2.chaoxing.com // @connect passport2-api.chaoxing.com // @connect mooc1.chaoxing.com // @connect mooc2-ans.chaoxing.com // @connect fanya.chaoxing.com // @connect *.fanya.chaoxing.com // @connect *.jxjy.chaoxing.com // @connect chaoxing.com.cn // @connect *.chaoxing.com.cn // @connect xueyinonline.com // @connect *.xueyinonline.com // @run-at document-end // @homepageURL https://wk.bobo91.com/ // @supportURL https://wk.bobo91.com/ // @icon https://wk.bobo91.com/logo.png // @license All Rights Reserved // ==/UserScript== // Copyright (c) 2026 木金. All rights reserved. // 未经作者许可,禁止复制、二次发布、倒卖或改名分发。 (function () { 'use strict'; const SCRIPT_VERSION = '3.3.18'; if (window.__mjn3_loaded__ === SCRIPT_VERSION) { return; } window.__mjn3_loaded__ = SCRIPT_VERSION; const CFG = { PREFIX: 'mjn3_', MONITOR_MS: 15000, TASK_DELAY_MS: 300, PAGE_LOAD_MS: 1200, POPUP_MS: 500, API_TIMEOUT: 10000, EXAM_COURSE_SCAN_LIMIT: 60, EXAM_SCAN_CONCURRENCY: 3, SWITCH_MS: 3000, STUCK_MAX: 30, STAGNATION_MAX: 90, MAX_NO_PROGRESS_MS: 60 * 60 * 1000, API_CHECK_INTERVAL_MS: 60000, TASK_RECHECK_MS: 120000, TASK_DONE_REENTER_MS: 12000, AUTO_FETCH_DELAY_MS: 1200, PROGRESS_LOG_MS: 1800000, HEARTBEAT_MS: 1800000, MIN_TASK_NAME_LEN: 2, MAX_TASK_NAME_LEN: 200, RELOGIN_MAX: 5, CAPTCHA_WAIT_S: 180, MONITORING_MIN_MS: 30000, FIRST_CHECK_MIN_MS: 60000, API_FAIL_MAX: 6, PROGRESS_REPORT_STEP: 5, CHAPTER_EXPAND_MS: 300, BODY_TEXT_MAX: 2000, TRANSITION_MIN_CD: 2, TRANSITION_CD_MS: 1000, TASK_TOAST_MS: 2000, TOAST_DURATION_MS: 2000, COURSE_BTN_DELAY_1: 2000, COURSE_BTN_DELAY_2: 5000, MIN_HTML_LEN: 200, MIN_ID_LEN: 3, NAV_CLICK_SLEEP: 800, ENTER_TASK_SLEEP: 80, FADE_OUT_MS: 300, NOTIFY_TIMEOUT: 5000, MIN_ENC_LEN: 32, QUEUE_PREVIEW_COUNT: 2, QUEUE_PREVIEW_LEN: 8, NAME_TRUNC_LEN: 14, POPUP_CLOSE_TEXTS: [ '同意', '确定', '知道了', '我知道了', '继续', '关闭', '×', '✕', '好的', '确认', 'OK', '开始学习', '进入学习', '我已阅读', '下一步', ], POPUP_CLOSE_SELECTORS: [ '.btn_blue', '.bluebtn', '.confirm', '.okBtn', '.posBtn', '.sureBtn', '[class*="confirm"]', '[class*="ok"]', '.layui-layer-btn0', '.dialog-btn', '.popDiv .btn', '.maskDiv .btn', '.ans-attach-btn', 'button[onclick*="close"]', '.popDiv button', '.maskDiv button', '.course-pop .btn', '.course-pop button', 'input[type="button"]', 'input[type="submit"]', ], POPUP_MASK_SELECTORS: '.maskDiv, .popBox, .layui-layer-shade, .popDiv, .course-pop, .coursenoticepop', }; const $ = (s, p) => (p || document).querySelector(s); const $$ = (s, p) => [...(p || document).querySelectorAll(s)]; const sleep = ms => new Promise(r => setTimeout(r, ms)); const log = (msg, lv) => { const level = lv ?? 'log'; const method = console[level] ?? console.log; method('[木金队列v3]', msg); }; function getUrlParam(name) { try { return new URL(location.href).searchParams.get(name); } catch (_) { return null; } } function getUrlParamFrom(url, name) { try { return new URL(url, location.href).searchParams.get(name) || ''; } catch (_) { return ''; } } function getUrlParams(url) { const params = {}; try { const u = new URL(url); for (const [k, v] of u.searchParams) { params[k] = v; } } catch (_) { /* 跨域或无效URL,忽略 */ } return params; } function isVisible(el) { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return false; const s = getComputedStyle(el); return s.display !== 'none' && s.visibility !== 'hidden'; } function detectPlatform() { const h = location.hostname; if (h.includes('chaoxing.com')) return 'chaoxing'; if (h.includes('zhihuishu.com')) return 'zhihuishu'; if (h.includes('icve.com.cn')) return 'icve'; if (h.includes('icourse163.org')) return 'mooc'; return 'unknown'; } const PLATFORM = detectPlatform(); const isTopFrame = window === window.top; function storeKey(k) { return CFG.PREFIX + k; } function loadQueue() { try { const r = GM_getValue(storeKey('queue_' + PLATFORM), '[]'); const a = JSON.parse(r); return Array.isArray(a) ? a : []; } catch (_) { log('loadQueue 存储异常,返回空队列', 'warn'); return []; } } function saveQueue(q) { try { GM_setValue(storeKey('queue_' + PLATFORM), JSON.stringify(q)); } catch (_) { log('saveQueue 存储异常', 'warn'); } } function loadState() { try { const r = GM_getValue(storeKey('state'), 'null'); return r ? JSON.parse(r) : null; } catch (_) { log('loadState 存储异常', 'warn'); return null; } } function saveState(s) { try { GM_setValue(storeKey('state'), JSON.stringify(s)); } catch (_) { log('saveState 存储异常', 'warn'); } } function clearState() { try { GM_deleteValue(storeKey('state')); } catch (_) { log('clearState 存储异常', 'warn'); } } function autoFetchKey() { return storeKey('auto_fetch_' + PLATFORM); } function requestAutoFetchCourses() { try { GM_setValue(autoFetchKey(), String(Date.now())); } catch (_) { /* 自动获取标记失败 */ } } function consumeAutoFetchCourses() { try { const v = GM_getValue(autoFetchKey(), ''); if (!v) return false; GM_deleteValue(autoFetchKey()); return true; } catch (_) { return false; } } function loadCourseHomeUrl() { try { return GM_getValue(storeKey('course_home_' + PLATFORM), '') || ''; } catch (_) { return ''; } } function saveCourseHomeUrl(url) { if (!url) return; try { GM_setValue(storeKey('course_home_' + PLATFORM), url); } catch (_) { /* 课程首页缓存失败 */ } } function getDefaultCourseHomeUrl() { if (PLATFORM === 'chaoxing') return 'https://mooc2-ans.chaoxing.com/mooc2-ans/visit/interaction'; return ''; } function getDefaultAccountHomeUrl() { if (PLATFORM === 'chaoxing') return 'https://i.mooc.chaoxing.com/space/index'; return getDefaultCourseHomeUrl(); } function isBadCourseHomeUrl(url) { try { const u = new URL(url, location.href); return /jxjy\.chaoxing\.com$/i.test(u.hostname) && /\/course-v2\/studyApp\/studying/i.test(u.pathname); } catch (_) { return false; } } const TASK_TYPE_MAP = { '1': '视频', '2': '文档', '3': '测验', '4': '作业', '5': '讨论', '6': '直播', '7': '考试', '8': '阅读', 'video': '视频', 'doc': '文档', 'document': '文档', 'test': '测验', 'exam': '考试', 'quiz': '测验', 'work': '作业', 'homework': '作业', 'discuss': '讨论', 'live': '直播', 'read': '阅读', }; function formatMinutes(minutes) { if (minutes < 60) return `${Math.floor(minutes)}分钟`; const h = Math.floor(minutes / 60); const m = Math.floor(minutes % 60); return m > 0 ? `${h}小时${m}分钟` : `${h}小时`; } function safeFetch(url, options = {}) { const ctrl = new AbortController(); const timeout = options.timeout ?? CFG.API_TIMEOUT; const tid = setTimeout(() => ctrl.abort(), timeout); const merged = { ...options, signal: ctrl.signal }; delete merged.timeout; return fetch(url, merged).finally(() => clearTimeout(tid)); } function canFetchSameOrigin(url) { try { return new URL(url, location.href).origin === location.origin; } catch (_) { return false; } } async function fetchAnyOrigin(url, options = {}) { if (canFetchSameOrigin(url)) { const resp = await safeFetch(url, { credentials: 'include', ...options }); if (!resp.ok) throw new Error('HTTP ' + resp.status); return { text: await resp.text(), finalUrl: resp.url || url }; } if (typeof GM_xmlhttpRequest === 'function') { const request = (currentUrl, redirects = 0) => new Promise((resolve, reject) => { try { GM_xmlhttpRequest({ method: options.method || 'GET', url: currentUrl, headers: options.headers || {}, data: options.body || options.data, anonymous: false, timeout: options.timeout || CFG.API_TIMEOUT, onload: resp => { const finalUrl = resp.finalUrl || resp.responseURL || currentUrl; if (resp.status >= 300 && resp.status < 400 && redirects < 3) { const loc = String(resp.responseHeaders || '').match(/(?:^|\n)\s*location\s*:\s*([^\n\r]+)/i); if (loc && loc[1]) { try { resolve(request(new URL(loc[1].trim(), currentUrl).href, redirects + 1)); return; } catch (_) { /* ignore */ } } } if (resp.status >= 200 && resp.status < 400) resolve({ text: resp.responseText || '', finalUrl }); else reject(new Error('HTTP ' + resp.status)); }, onerror: () => reject(new Error('请求失败')), ontimeout: () => reject(new Error('请求超时')), }); } catch (e) { reject(e); } }); return request(url); } throw new Error('跨域读取不可用'); } async function fetchTextAnyOrigin(url, options = {}) { return (await fetchAnyOrigin(url, options)).text; } function parseStudystateData(el) { if (!el) return null; const raw = (el.value || '').replace(/"/g, '"'); let data = null; try { data = JSON.parse(raw); } catch (_) { try { data = JSON.parse(raw.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')); } catch (__) { /* JSON解析失败 */ } } return data; } function getAllDocs() { const docs = [document]; for (const iframe of $$('iframe')) { try { const doc = iframe.contentDocument || iframe.contentWindow?.document; if (doc?.body) docs.push(doc); } catch (_) { /* 跨域iframe,忽略 */ } } return docs; } function getChaoxingPageEnc() { let enc = getUrlParam('enc') || ''; if (enc) return enc; try { for (const doc of getAllDocs()) { for (const s of doc.querySelectorAll('script')) { const match = (s.textContent || '').match(/(?:var|let|const)\s+enc\s*=\s*["']([a-f0-9]{32})["']/); if (match) return match[1]; } } } catch (_) { /* ignore */ } try { if (typeof unsafeWindow !== 'undefined' && typeof unsafeWindow.toOld === 'function') { const match = unsafeWindow.toOld.toString().match(/enc\s*=\s*"([a-f0-9]{32})"/); if (match) return match[1]; } } catch (_) { /* ignore */ } return ''; } function normText(text) { return (text || '').trim().replace(/\s+/g, ' '); } function cleanExtractedUrl(url) { return String(url || '') .replace(/&/g, '&') .trim() .replace(/(?:%27|%22|['")\s])+$/g, ''); } function getAllUiDocs() { const docs = []; const seen = new Set(); const collect = doc => { if (!doc || seen.has(doc)) return; seen.add(doc); docs.push(doc); for (const iframe of doc.querySelectorAll('iframe')) { try { collect(iframe.contentDocument || iframe.contentWindow?.document); } catch (_) { /* cross-origin iframe */ } } }; collect(document); return docs; } function classText(el) { if (!el) return ''; const cls = typeof el.className === 'string' ? el.className : (el.className?.baseVal || ''); return String(cls).toLowerCase(); } function getTaskCompletionStatus(item, iconEl) { const text = normText(item?.textContent || ''); const unfinishEl = item?.querySelector?.('.jobUnfinishCount, [class*="Unfinish"], [class*="unfinish"], [class*="unfinished"]'); if (unfinishEl) { const raw = normText(unfinishEl.textContent || unfinishEl.value || ''); const count = parseInt(raw.replace(/[^\d]/g, '') || '0', 10); return count > 0 ? 'todo' : 'done'; } if (/未完成|未学习|待完成|待学习|未通过|进行中/.test(text)) return 'todo'; if (/已完成|已通过|已学习|已观看|完成度\s*100%|100%\s*完成/.test(text)) return 'done'; const cls = `${classText(item)} ${classText(iconEl)}`; if (/(unfinish|unfinished|notfinish|not-finish|todo|doing|progress)/.test(cls)) return 'todo'; if (/(finish|finished|done|passed|complete|completed|correct|success|studied|checked)/.test(cls)) return 'done'; return 'unknown'; } function buildTaskCandidate(name, taskExtra = {}) { return { name, knowledgeId: taskExtra.knowledgeId || '', chapterName: taskExtra.chapterName || '', chapterId: taskExtra.chapterId || '', isDone: false, type: taskExtra.type || '', element: taskExtra.element, clickTarget: taskExtra.clickTarget, courseId: taskExtra.courseId || '', clazzid: taskExtra.clazzid || '', cpi: taskExtra.cpi || '', enc: taskExtra.enc || '', }; } function pickNextTask(tasks) { const unfinished = (tasks || []).filter(t => !t.isDone); if (unfinished.length === 0) return null; return unfinished.find(t => t.knowledgeId && String(t.knowledgeId).length > CFG.MIN_ID_LEN) || unfinished[0]; } function getCourseParams(task) { return { cid: task?.courseId || getUrlParam('courseId') || getUrlParam('courseid') || getUrlParam('cid') || '', clid: task?.clazzid || getUrlParam('clazzid') || getUrlParam('clazzId') || '', cpi: task?.cpi || getUrlParam('cpi') || '', enc: task?.enc || getUrlParam('enc') || '', }; } const PageDetector = { chaoxing() { const u = location.href; if (u.includes('/visit/interaction') && !u.includes('courseid=')) return 'courseList'; if (u.includes('/visit/stucoursemiddle') && !u.includes('courseid=')) return 'courseList'; if (/mooc2-ans\.chaoxing\.com\/mooc2-ans\/?$/.test(u)) return 'courseList'; if (u.includes('knowledgeid=') || u.includes('knowledgeId=')) return 'taskPage'; if (u.includes('/mycourse/studentstudy')) return 'taskPage'; if (u.includes('/visit/interaction') && u.includes('courseid=')) return 'courseDetail'; if (u.includes('/ananas/modules/') || u.includes('/work/')) return 'taskPage'; if ((u.includes('courseId=') || u.includes('courseid=') || u.includes('clazzid=')) && u.includes('/stu')) return 'courseDetail'; if (u.includes('/mycourse/studentcourse')) return 'courseDetail'; if (u.includes('/visit/stucoursemiddle') && u.includes('courseid=')) return 'courseDetail'; return 'unknown'; }, zhihuishu() { const u = location.href; if (u.includes('onlinestuh5') && !u.includes('/stu/study')) return 'courseList'; if (u.includes('onlineweb') && !u.includes('/study')) return 'courseList'; if (u.includes('/student/courseList')) return 'courseList'; if (u.includes('/stu/study') || u.includes('/study/video')) return 'taskPage'; if (u.includes('courseId') || u.includes('recruitId') || u.includes('shareId')) return 'courseDetail'; return 'unknown'; }, icve() { const u = location.href; if (u.includes('/student/course') || u.includes('/center/')) return 'courseList'; if (u.includes('/study/') || u.includes('/learn/') || u.includes('/video/')) return 'taskPage'; if (u.includes('courseId=') || u.includes('courseid=') || u.includes('/course/')) return 'courseDetail'; return 'unknown'; }, mooc() { const u = location.href; if (/icourse163\.org\/?$/.test(u) || u.includes('/home/') || u.includes('/course/')) return 'courseList'; if (u.includes('/learn/') && u.includes('#/learn/content')) return 'taskPage'; if (u.includes('/learn/')) return 'courseDetail'; return 'unknown'; }, detect() { return (this[PLATFORM] || (() => 'unknown')).call(this); }, }; function dismissPopups() { for (const sel of CFG.POPUP_CLOSE_SELECTORS) { for (const el of $$(sel)) { if (!isVisible(el)) continue; const t = (el.textContent || el.value || '').trim(); if (CFG.POPUP_CLOSE_TEXTS.some(k => t.includes(k))) { try { el.click(); log('关闭弹窗: ' + t); } catch (_) { /* 点击失败 */ } return; } } } for (const el of $$(CFG.POPUP_MASK_SELECTORS)) { if (isVisible(el)) { try { el.style.display = 'none'; } catch (_) { /* 样式修改失败 */ } const closeBtn = el.querySelector('.layui-layer-close, [class*="close"]'); if (closeBtn && isVisible(closeBtn)) { try { closeBtn.click(); } catch (_) { /* 关闭按钮点击失败 */ } } } } } function checkLoginStatus() { const u = location.href.toLowerCase(); if (u.includes('passport') || u.includes('login') || u.includes('signin')) return true; const bodyText = document.body ? (document.body.innerText || '').substring(0, CFG.BODY_TEXT_MAX) : ''; if (bodyText.includes('请先登录') || bodyText.includes('登录超时') || bodyText.includes('session失效') || bodyText.includes('session过期') || bodyText.includes('身份过期')) return true; for (const sel of ['[class*="dialog"][class*="login"]', '[class*="modal"][class*="login"]', '[role="dialog"]']) { for (const el of $$(sel)) { if (isVisible(el) && (el.textContent || '').toLowerCase().includes('登录')) return true; } } return false; } function isChaoxingErrorPage() { const title = String(document.title || '').toLowerCase(); const text = String(document.body?.innerText || '').slice(0, CFG.BODY_TEXT_MAX); return title.includes('404') || text.includes('(404)') || text.includes('页面不存在') || text.includes('您所浏览的页面不存在'); } function clickChapterTab() { const tabs = document.querySelectorAll('a, span, li, div, button, [role="tab"]'); for (const tab of tabs) { const t = (tab.textContent || '').trim(); if (t === '章节' || t === '课程章节' || t === '课程内容' || t === '目录' || t === '课程目录') { try { tab.click(); log('点击章节标签: ' + t); return t; } catch (_) { /* 点击失败 */ } } } const sidebarLinks = document.querySelectorAll('a[href*="chapter"], a[href*="catalog"], a[href*="knowledge"]'); for (const link of sidebarLinks) { const t = (link.textContent || '').trim(); if (t.includes('章节') || t.includes('目录') || t.includes('课程内容')) { try { link.click(); return 'link:' + t; } catch (_) { /* 点击失败 */ } } } return null; } function fixCourseUrl(href) { if (!href) return href; if (href.includes('/mycourse/studentstudy')) return href; if (href.includes('/visit/stucoursemiddle')) return href; href = href.replace('mooc1-1.chaoxing.com', 'mooc2-ans.chaoxing.com'); href = href.replace('mooc1.chaoxing.com', 'mooc2-ans.chaoxing.com'); href = href.replace('i.chaoxing.com', 'mooc2-ans.chaoxing.com'); href = href.replace('/mooc-ans/', '/mooc2-ans/'); if (href.includes('mooc2-ans.chaoxing.com') && !href.includes('mooc2-ans.chaoxing.com/mooc2-ans/')) { href = href.replace('mooc2-ans.chaoxing.com/', 'mooc2-ans.chaoxing.com/mooc2-ans/'); } return href; } function buildCourseUrl(url) { if (!url) return url; const cid = url.match(/courseid=(\d+)/i) || url.match(/courseId=(\d+)/i); const clid = url.match(/clazzid=(\d+)/i) || url.match(/clazzId=(\d+)/i); if (cid && clid) { const cpi = url.match(/[&?]cpi=(\d+)/i); const enc = url.match(/[&?]enc=([a-f0-9]+)/i); let newUrl = `https://mooc1.chaoxing.com/visit/stucoursemiddle?courseid=${cid[1]}&clazzid=${clid[1]}`; if (cpi) newUrl += `&cpi=${cpi[1]}`; if (enc) newUrl += `&enc=${enc[1]}`; newUrl += '&ismooc2=1&v=2'; return newUrl; } return url; } function isProbablyACourse(text, href) { text = (text || '').trim(); href = href || ''; const excludeTexts = [ '进入空间', '账号管理', '已删除课程', '已结束课程', '已退课课程', '课程已结束', '更新公告', '我的课', '我教的课', '添加课程', '新建文件夹', '学习通', '客服', '帮助', '设置', '退出', '登录', '注册', '空间', '消息', '通知', '作业', '考试', '讨论', ]; for (const ex of excludeTexts) { if (text.includes(ex)) return false; } const hasCoursePath = href.includes('studentcourse') || href.includes('stucoursemiddle'); if (!hasCoursePath) return false; const hasCourseId = href.includes('courseId') || href.includes('courseid') || href.includes('clazzid') || href.includes('clazzId'); return hasCourseId; } function buildCourseEntry(el, baseUrl, seen) { const rawHref = el.getAttribute('href') || ''; let href = ''; if (rawHref) { try { href = new URL(rawHref, baseUrl).href; } catch (_) { return null; } } const text = (el.textContent || '').trim().replace(/\s+/g, ' '); const fixed = fixCourseUrl(href); if (!href || !text || seen.has(fixed) || text.length < CFG.MIN_TASK_NAME_LEN || text.length > CFG.MAX_TASK_NAME_LEN) return null; if (!isProbablyACourse(text, fixed)) return null; seen.add(fixed); return { name: text, url: fixed, platform: 'chaoxing' }; } function getUrlFromOnclick(onclick, baseUrl) { if (!onclick) return ''; const direct = onclick.match(/https?:\/\/[^'")\s]+/); if (direct) { try { return new URL(cleanExtractedUrl(direct[0]), baseUrl).href; } catch (_) { return cleanExtractedUrl(direct[0]); } } const quotedArgs = [...onclick.matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]); const urlArg = quotedArgs.find(arg => /^(\/|https?:\/\/)/.test(arg) || /\/ks\/|exam|paper|test/i.test(arg)); if (urlArg) { try { return new URL(cleanExtractedUrl(urlArg), baseUrl).href; } catch (_) { return ''; } } const quoted = onclick.match(/(?:open|href|setUrl|toExam|goExam|startExam)[^(]*\((?:[^'"]*['"]){1}([^'"]+)/i); if (quoted && quoted[1]) { try { return new URL(cleanExtractedUrl(quoted[1]), baseUrl).href; } catch (_) { return ''; } } return ''; } function splitJsArgs(src) { const args = []; let cur = ''; let quote = ''; let escaped = false; for (const ch of String(src || '')) { if (escaped) { cur += ch; escaped = false; continue; } if (ch === '\\') { cur += ch; escaped = true; continue; } if (quote) { cur += ch; if (ch === quote) quote = ''; continue; } if (ch === '\'' || ch === '"') { quote = ch; cur += ch; continue; } if (ch === ',') { args.push(cur.trim()); cur = ''; continue; } cur += ch; } if (cur.trim() || src) args.push(cur.trim()); return args.map(v => { const s = String(v || '').trim(); if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"'))) { return s.slice(1, -1).replace(/\\(['"\\])/g, '$1'); } return s; }); } function getDocValue(doc, names) { for (const name of names) { const byId = doc?.getElementById?.(name); if (byId?.value) return String(byId.value).trim(); const byName = doc?.querySelector?.(`input[name="${name}"], textarea[name="${name}"], select[name="${name}"]`); if (byName?.value) return String(byName.value).trim(); } return ''; } function getUrlParamFrom(baseUrl, names) { try { const u = new URL(baseUrl, location.href); for (const name of names) { const v = u.searchParams.get(name); if (v) return v; } } catch (_) { /* ignore */ } return ''; } function getUrlFromGoTest(onclick, baseUrl, doc) { if (!/goTest\s*\(/i.test(onclick || '')) return ''; const match = String(onclick || '').match(/goTest\s*\(([\s\S]*?)\)\s*;?/i); if (!match) return ''; const args = splitJsArgs(match[1]); const courseId = args[0] || getDocValue(doc, ['courseId', 'courseid']) || getUrlParamFrom(baseUrl, ['courseId', 'courseid']); const examId = args[1] || ''; const isRetest = /^(true|1)$/i.test(String(args[5] || '')); const classId = getDocValue(doc, ['classId', 'clazzId', 'clazzid']) || getUrlParamFrom(baseUrl, ['classId', 'clazzId', 'clazzid']); const cpi = getDocValue(doc, ['cpi']) || getUrlParamFrom(baseUrl, ['cpi']) || '0'; if (!courseId || !classId || !examId) return ''; try { const u = new URL('/exam-ans/exam/test/examcode/examnotes', baseUrl); u.searchParams.set('courseId', courseId); u.searchParams.set('classId', classId); u.searchParams.set('examId', examId); u.searchParams.set('cpi', cpi); if (isRetest) u.searchParams.set('reset', 'true'); return u.href; } catch (_) { return ''; } } function isExamListPageUrl(href) { try { const u = new URL(href, location.href); return /examSysList/i.test(u.pathname) || /\/exam-ans\/mooc2\/exam\/exam-list/i.test(u.pathname); } catch (_) { return /examSysList|\/exam-ans\/mooc2\/exam\/exam-list/i.test(href || ''); } } function isExamJunkText(rowText, actionText) { const row = normText(rowText); const action = normText(actionText); const single = /^(提示|确定|取消|知道了|退出|退出考试|申诉|重新识别|继续作答|重刷|删除|确定重考)$/; if (single.test(action)) return true; if (/^(提示\s*)?(确定|取消|知道了|退出|退出考试|申诉|重新识别|继续作答|重刷|删除|确定重考)(\s+(确定|取消|申诉|退出考试|继续作答|重刷))*$/.test(row)) return true; return /退出考试将强制收卷|设备已锁定|识别结果|删除后将无法恢复|系统检测到|错题练习|直播二维码|延时提醒|锁定原因/.test(row); } function isExamJunkElement(el, row) { const target = el?.closest?.('.maskDiv, .popDiv, .popBottom, .popHead, .deviceLockPop, [id*="Pop"], [id*="Win"], [id*="Tip"]'); if (!target) return false; const cls = `${classText(target)} ${classText(row)}`; const id = `${target.id || ''} ${row?.id || ''}`; return /maskDiv|popDiv|popBottom|popHead|deviceLock|confirm|appeal|forceSubmit|submit|Lock|Pop|Win|Tip/i.test(`${cls} ${id}`); } function isProbablyExam(text, href, actionText) { const hay = `${text || ''} ${href || ''} ${actionText || ''}`.toLowerCase(); text = text || ''; actionText = actionText || ''; if (/进入空间|账号管理|退出登录|输入邀请码|课程队列|考试队列|获取考试|添加选中|开始学习/.test(text)) return false; const hrefPath = (() => { try { return new URL(href, location.href).pathname; } catch (_) { return href || ''; } })(); if (/examSysList|\/exam-ans\/mooc2\/exam\/exam-list|\/keeper\/appeal/i.test(hrefPath)) return false; const hasExamWord = /考试|期末|测验/.test(text) || /进入考试|开始考试|立即考试|继续考试/.test(actionText) || /\/ks\/|exam|paper|test|examcode|mycourse\/transfer|gotest|icon-exam/.test(hay); if (!hasExamWord) return false; if (/总评|成绩查询|收件箱|课程队列|获取课程|添加选中|开始学习/.test(text)) return false; if (/查看成绩|查看试卷|已交卷|已完成|已结束|缺考/.test(text) && !/未开始|待考|进入考试|开始考试/.test(text)) return false; return true; } function buildExamEntry(el, baseUrl, seen) { const actionText = normText(el.textContent || el.value || el.title || ''); const onclick = el.getAttribute?.('onclick') || ''; const rawHref = el.getAttribute?.('href') || ''; let href = ''; if (rawHref && rawHref !== '#' && !/^javascript:/i.test(rawHref)) { try { href = new URL(rawHref, baseUrl).href; } catch (_) { href = ''; } } if (!href && /^javascript:/i.test(rawHref)) href = getUrlFromOnclick(rawHref, baseUrl); if (!href) href = getUrlFromOnclick(onclick, baseUrl); if (!href) href = getUrlFromGoTest(onclick, baseUrl, el.ownerDocument || document); if (!href) return null; const row = el.closest?.('.bottomList li, tr, li, .item, .list-item, .exam-item, .examList, .ks-item, .clearfix, .table-row, .content, .box') || el.parentElement || el; const rowText = normText(row.textContent || actionText); if (isExamListPageUrl(href) || isExamJunkElement(el, row) || isExamJunkText(rowText, actionText)) return null; if (!isProbablyExam(rowText, href, actionText)) return null; const cardName = normText(row.querySelector?.('.overHidden2, .right-content p:first-child')?.textContent || ''); let name = (cardName || rowText) .replace(/^(进入考试|开始考试|立即考试|查看|操作)\s*/g, '') .replace(/\s*(进入考试|开始考试|立即考试|查看|操作)\s*$/g, '') .trim(); const parts = rowText.split(/\s+/).filter(Boolean); const dateIdx = parts.findIndex(p => /^\d{4}-\d{2}-\d{2}$/.test(p)); if (dateIdx > 1) { const nameParts = parts.slice(1, dateIdx); if (nameParts.length >= 2 && nameParts[0] === nameParts[1]) name = nameParts[0]; else if (nameParts.length > 0) name = nameParts.join(' '); } if (!name || name.length < CFG.MIN_TASK_NAME_LEN) name = actionText || '未命名考试'; if (name.length > 120) name = name.slice(0, 120); const key = href.replace(/([?&])t=\d+/g, '$1').replace(/([?&])_=\d+/g, '$1'); if (seen.has(key)) return null; seen.add(key); return { name, url: href, platform: PLATFORM, addedAt: Date.now(), }; } const ChaoxingAdapter = { getCourseHomeCandidates() { const urls = []; const push = raw => { if (!raw) return; try { const href = new URL(cleanExtractedUrl(raw), location.href).href; if (/\/visit\/interaction|\/visit\/stucoursemiddle|\/mooc2-ans\/visit\//i.test(href)) urls.push(href); } catch (_) { /* ignore */ } }; push(location.href); push(document.referrer); for (const iframe of $$('iframe')) { push(iframe.src || iframe.getAttribute('src') || ''); } for (const el of $$('a[href], [onclick], [data-url]')) { const rawHref = el.getAttribute('href') || ''; const dataUrl = el.getAttribute('data-url') || ''; const onclick = el.getAttribute('onclick') || rawHref; push(rawHref); push(dataUrl); push(getUrlFromOnclick(onclick, location.href)); } push(getDefaultCourseHomeUrl()); return [...new Set(urls)]; }, parseCoursesFromDoc(doc, baseUrl, seen = new Set()) { const courses = []; const selectors = [ 'a[href*="studentcourse"]', 'a[href*="stucoursemiddle"]', 'a[href*="courseId"]', 'a[href*="courseid"]', 'a[href*="clazzid"]', 'a[href*="clazzId"]', ]; for (const sel of selectors) { for (const el of doc.querySelectorAll(sel)) { const entry = buildCourseEntry(el, baseUrl, seen); if (entry) courses.push(entry); } } return courses; }, async fetchCoursesFromCourseHomePages() { const courses = []; const seen = new Set(); for (const fetchUrl of this.getCourseHomeCandidates()) { try { const fetched = await fetchAnyOrigin(fetchUrl, { timeout: CFG.API_TIMEOUT }); const doc = new DOMParser().parseFromString(fetched.text, 'text/html'); courses.push(...this.parseCoursesFromDoc(doc, fetched.finalUrl || fetchUrl, seen)); if (courses.length > 0) break; } catch (e) { log(`课程首页扫描失败: ${fetchUrl} - ${e.message}`, 'warn'); } } return courses; }, async fetchCoursesViaAPI() { const courses = []; const seen = new Set(); const urls = [...new Set([ ...this.getCourseHomeCandidates(), 'https://mooc2-ans.chaoxing.com/mooc2-ans/visit/interaction', 'https://mooc2-ans.chaoxing.com/mooc2-ans/visit/stucoursemiddle', ])]; for (const fetchUrl of urls) { try { const fetched = await fetchAnyOrigin(fetchUrl, { timeout: CFG.API_TIMEOUT }); const doc = new DOMParser().parseFromString(fetched.text, 'text/html'); courses.push(...this.parseCoursesFromDoc(doc, fetched.finalUrl || fetchUrl, seen)); if (courses.length > 0) break; } catch (_) { /* 网络请求失败 */ } } if (courses.length === 0) { return this.parseCourseList(); } return courses; }, switchToNewVersion() { const u = location.href; if (u.includes('mooc2-ans')) return true; if (u.includes('/mycourse/studentstudy')) return true; if (u.includes('/visit/stucoursemiddle') && u.includes('ismooc2=1')) return true; if (u.includes('mooc1') || u.includes('mooc-ans') || u.includes('i.chaoxing.com')) { const newUrl = fixCourseUrl(u); if (newUrl !== u) { log('切换新版: ' + newUrl); location.href = newUrl; return true; } } for (const iframe of $$('iframe')) { try { const src = iframe.src || ''; if (src.includes('/mycourse/studentstudy')) continue; if (src.includes('mooc1') || src.includes('mooc-ans') || src.includes('visit/interaction')) { const newUrl = fixCourseUrl(src); if (newUrl !== src) { log('从iframe切换新版: ' + newUrl); location.href = newUrl; return true; } } } catch (_) { /* 跨域iframe */ } } return false; }, parseCourseList() { const courses = [], seen = new Set(); const baseUrl = location.href; const selectors = ['a[href*="studentcourse"]', 'a[href*="stucoursemiddle"]', 'a[href*="courseId"]', 'a[href*="courseid"]', 'a[href*="clazzid"]', 'a[href*="clazzId"]']; for (const sel of selectors) { for (const el of $$(sel)) { const entry = buildCourseEntry(el, baseUrl, seen); if (entry) courses.push(entry); } } if (courses.length === 0) { for (const el of $$('a')) { const entry = buildCourseEntry(el, baseUrl, seen); if (entry) courses.push(entry); } } if (courses.length === 0) { for (const iframe of $$('iframe')) { try { const doc = iframe.contentDocument || iframe.contentWindow?.document; if (!doc) continue; const iframeSrc = iframe.src || baseUrl; for (const el of doc.querySelectorAll('a')) { const entry = buildCourseEntry(el, iframeSrc, seen); if (entry) courses.push(entry); } } catch (_) { /* 跨域iframe */ } } } return courses; }, getCourseHomeUrl() { const defaultHome = getDefaultCourseHomeUrl(); const candidates = [location.href, document.referrer].filter(Boolean); for (const iframe of $$('iframe')) { const src = iframe.src || iframe.getAttribute('src') || ''; if (src) candidates.push(src); } for (const el of $$('a[href], [onclick]')) { const rawHref = el.getAttribute('href') || ''; const onclick = el.getAttribute('onclick') || ''; if (rawHref) candidates.push(rawHref); const fromClick = getUrlFromOnclick(onclick, location.href); if (fromClick) candidates.push(fromClick); } try { const qback = new URL(location.href).searchParams.get('qbankbackurl'); if (qback) candidates.push(decodeURIComponent(qback)); } catch (_) { /* ignore */ } const normalizeHome = raw => { if (!raw) return ''; let u; try { u = new URL(cleanExtractedUrl(raw), location.href); } catch (_) { return ''; } if (isBadCourseHomeUrl(u.href)) return ''; if (/\/(?:course-v2\/)?studyApp\/studying/i.test(u.pathname)) { if (!/\/course-v2\/studyApp\/studying/i.test(u.pathname)) u.pathname = '/course-v2/studyApp/studying'; return u.href; } const s = u.searchParams.get('s'); if (s && /jxjy\.chaoxing\.com$/i.test(u.hostname)) { return ''; } return ''; }; for (const raw of candidates) { const home = normalizeHome(raw); if (home) { saveCourseHomeUrl(home); return home; } } const saved = loadCourseHomeUrl(); if (saved && !isBadCourseHomeUrl(saved)) return saved; return defaultHome || 'https://i.mooc.chaoxing.com/space/index'; }, goCourseHome() { const url = getDefaultAccountHomeUrl() || this.getCourseHomeUrl(); if (url) location.href = url; }, getExamListUrl() { const frameSelectors = [ 'iframe[src*="examSysList"]', 'iframe[src*="/ks/xs/"]', 'iframe[src*="exam-v2"]', 'iframe[src*="/exam-ans/mooc2/exam/exam-list"]', 'iframe[src*="exam-list"]', ]; for (const sel of frameSelectors) { for (const iframe of $$(sel)) { const src = iframe.src || iframe.getAttribute('src') || ''; if (!src) continue; try { return new URL(cleanExtractedUrl(src), location.href).href; } catch (_) { /* ignore */ } } } const selectors = [ '[onclick*="examSysList"]', 'a[href*="examSysList"]', '[onclick*="/ks/xs/"]', 'a[href*="/ks/xs/"]', '[data-url*="exam-list"]', 'a[href*="exam-list"]', '[onclick*="exam-list"]', ]; for (const sel of selectors) { for (const el of $$(sel)) { const text = normText(el.textContent || el.title || ''); const onclick = el.getAttribute('onclick') || ''; const rawHref = el.getAttribute('href') || ''; const rawDataUrl = el.getAttribute('data-url') || ''; let href = ''; if (rawHref && /^javascript:/i.test(rawHref)) href = getUrlFromOnclick(rawHref, location.href); if (!href && rawHref) href = rawHref; if (!href && rawDataUrl) href = rawDataUrl; if (!href) href = getUrlFromOnclick(onclick, location.href); if (!href) continue; href = cleanExtractedUrl(href); if (text.includes('我的考试') || text.includes('在线考试') || href.includes('examSysList') || href.includes('/ks/xs/') || href.includes('exam-list')) { try { return new URL(href, location.href).href; } catch (_) { /* ignore */ } } } } return ''; }, parseExamList(doc = document, baseUrl = location.href) { const exams = []; const seen = new Set(); const addEntry = el => { const entry = buildExamEntry(el, baseUrl, seen); if (entry) exams.push(entry); }; const rowSelectors = [ '.bottomList li', '.gtestlistul li.gclassitems2:not(.gclasslisttitle)', 'li.gclassitems2:not(.gclasslisttitle)', 'tr', ]; for (const row of doc.querySelectorAll(rowSelectors.join(','))) { const rowText = normText(row.textContent || ''); if (!rowText) continue; if (/已结束|已提交|已交卷|查看成绩|查看试卷|缺考/.test(rowText) && !/开始考试|进入考试|继续考试/.test(rowText)) continue; const actions = row.querySelectorAll('[onclick*="toExam"], [onclick*="goExam"], [onclick*="startExam"], [onclick*="goTest"], [onclick*="exam"], a[href*="exam"], a[href*="test"], button, a'); for (const el of actions) addEntry(el); } if (exams.length > 0) return exams; const selectors = [ '[onclick*="toExam"]', '[onclick*="goExam"]', '[onclick*="startExam"]', '[onclick*="goTest"]', 'a[href*="/ks/"]', 'a[href*="exam"]', 'a[href*="paper"]', '[onclick*="/ks/"]', '[onclick*="exam"]', '[onclick*="paper"]', 'button', 'a[href]', ]; for (const sel of selectors) { for (const el of doc.querySelectorAll(sel)) { addEntry(el); } } return exams; }, async fetchCoursesFromCourseListData() { const read = (id, fallback = '') => document.getElementById(id)?.value || fallback; const courseType = read('courseType', '1'); const pageHeader = courseType === '1' ? read('stuPageHeader', '-1') : read('tchPageHeader', '-1'); const params = new URLSearchParams({ courseType, courseFolderId: read('courseFolderId', '0'), query: read('searchInput', ''), pageHeader, single: read('single', '0'), superstarClass: read('superstarClass', '0'), isFirefly: read('isFirefly', '0') === '1' ? '1' : '0', }); const html = await fetchTextAnyOrigin('https://mooc2-ans.chaoxing.com/mooc2-ans/visit/courselistdata', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString(), timeout: CFG.API_TIMEOUT, }); const doc = new DOMParser().parseFromString(html, 'text/html'); const courses = []; const seen = new Set(); const selectors = [ 'a[href*="stucoursemiddle"]', 'a[href*="studentcourse"]', 'a[href*="courseid"][href*="clazzid"]', 'a[href*="courseId"][href*="clazzId"]', ]; for (const sel of selectors) { for (const el of doc.querySelectorAll(sel)) { const entry = buildCourseEntry(el, 'https://mooc2-ans.chaoxing.com/mooc2-ans/visit/courselistdata', seen); if (entry) courses.push(entry); } } return courses; }, extractCourseStuUrl(html, baseUrl) { const text = String(html || '').replace(/&/g, '&').replace(/\\u0026/g, '&'); const patterns = [ /https?:\/\/[^"'<>]+\/mooc2-ans\/mycourse\/stu\?[^"'<>]+/i, /\/mooc2-ans\/mycourse\/stu\?[^"'<>]+/i, ]; for (const re of patterns) { const m = text.match(re); if (!m) continue; try { return new URL(cleanExtractedUrl(m[0]), baseUrl).href; } catch (_) { /* ignore */ } } return ''; }, buildExamListUrlFromCourseDoc(doc, baseUrl) { const iframe = doc.querySelector('iframe[src*="/exam-ans/mooc2/exam/exam-list"], iframe[src*="exam-list"]'); if (iframe) { const src = iframe.src || iframe.getAttribute('src') || ''; if (src) { try { return new URL(cleanExtractedUrl(src), baseUrl).href; } catch (_) { /* ignore */ } } } const link = doc.querySelector('[data-url*="/exam-ans/mooc2/exam/exam-list"], [data-url*="exam-list"], a[href*="exam-list"]'); const raw = link?.getAttribute?.('data-url') || link?.getAttribute?.('href') || 'https://mooc1.chaoxing.com/exam-ans/mooc2/exam/exam-list'; const courseId = getDocValue(doc, ['courseid', 'courseId']) || getUrlParamFrom(baseUrl, ['courseid', 'courseId']); const clazzid = getDocValue(doc, ['clazzid', 'clazzId', 'classId']) || getUrlParamFrom(baseUrl, ['clazzid', 'clazzId', 'classId']); const cpi = getDocValue(doc, ['cpi']) || getUrlParamFrom(baseUrl, ['cpi']) || ''; if (!courseId || !clazzid) return ''; try { const u = new URL(cleanExtractedUrl(raw), baseUrl); u.searchParams.set('courseid', courseId); u.searchParams.set('clazzid', clazzid); if (cpi) u.searchParams.set('cpi', cpi); u.searchParams.set('ut', getDocValue(doc, ['heardUt', 'ut']) || getUrlParamFrom(baseUrl, ['ut']) || 's'); u.searchParams.set('t', getDocValue(doc, ['t']) || getUrlParamFrom(baseUrl, ['t']) || String(Date.now())); const stuenc = getDocValue(doc, ['enc']) || getUrlParamFrom(baseUrl, ['enc', 'stuenc']); const examEnc = getDocValue(doc, ['examEnc']) || getUrlParamFrom(baseUrl, ['examEnc']); const openc = getDocValue(doc, ['openc']) || getUrlParamFrom(baseUrl, ['openc']); if (stuenc) u.searchParams.set('stuenc', stuenc); if (examEnc) u.searchParams.set('enc', examEnc); if (openc) u.searchParams.set('openc', openc); return u.href; } catch (_) { return ''; } }, async fetchCourseExams(course, seen) { const urls = [...new Set([course.url, buildCourseUrl(course.url)].filter(Boolean))]; for (const detailUrl of urls) { try { let fetched = await fetchAnyOrigin(detailUrl, { timeout: CFG.API_TIMEOUT }); let html = fetched.text; let currentUrl = fetched.finalUrl || detailUrl; let doc = new DOMParser().parseFromString(html, 'text/html'); let examListUrl = this.buildExamListUrlFromCourseDoc(doc, currentUrl); if (!examListUrl) { const stuUrl = this.extractCourseStuUrl(html, currentUrl); if (stuUrl) { fetched = await fetchAnyOrigin(stuUrl, { timeout: CFG.API_TIMEOUT }); html = fetched.text; currentUrl = fetched.finalUrl || stuUrl; doc = new DOMParser().parseFromString(html, 'text/html'); examListUrl = this.buildExamListUrlFromCourseDoc(doc, currentUrl); } } if (!examListUrl) continue; const listHtml = await fetchTextAnyOrigin(examListUrl, { timeout: CFG.API_TIMEOUT }); const listDoc = new DOMParser().parseFromString(listHtml, 'text/html'); const exams = this.parseExamList(listDoc, examListUrl); const picked = []; for (const exam of exams) { const key = exam.url.replace(/([?&])t=\d+/g, '$1').replace(/([?&])_=\d+/g, '$1'); if (seen.has(key)) continue; seen.add(key); const name = course.name && !exam.name.includes(course.name) ? `${course.name} - ${exam.name}` : exam.name; picked.push({ ...exam, name, courseName: course.name, courseUrl: course.url }); } if (picked.length > 0) return picked; } catch (e) { log(`课程考试扫描失败: ${course.name || detailUrl} - ${e.message}`, 'warn'); } } return []; }, async fetchExamsFromCourseList() { let courses = []; try { courses = await this.fetchCoursesFromCourseListData(); } catch (e) { log('课程列表考试扫描失败: ' + e.message, 'warn'); } if (!courses || courses.length === 0) { try { courses = await this.fetchCoursesFromCourseHomePages(); } catch (e) { log('课程首页考试扫描失败: ' + e.message, 'warn'); } } if (!courses || courses.length === 0) courses = this.parseCourseList ? this.parseCourseList() : []; const seen = new Set(); const exams = []; const limited = courses.slice(0, CFG.EXAM_COURSE_SCAN_LIMIT); const size = Math.max(1, CFG.EXAM_SCAN_CONCURRENCY || 1); for (let i = 0; i < limited.length; i += size) { const batch = await Promise.all(limited.slice(i, i + size).map(course => this.fetchCourseExams(course, seen))); for (const found of batch) exams.push(...found); } return exams; }, async fetchExamsViaAPI() { let exams = this.parseExamList(document, location.href); if (exams.length > 0 && !exams.every(e => e.url.includes('examSysList'))) return exams; const listUrl = this.getExamListUrl(); if (!listUrl) { const courseListExams = await this.fetchExamsFromCourseList(); if (courseListExams.length > 0) return courseListExams; return exams.filter(e => !e.url.includes('examSysList') && !isExamListPageUrl(e.url)); } try { const html = await fetchTextAnyOrigin(listUrl, { timeout: CFG.API_TIMEOUT }); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const fetched = this.parseExamList(doc, listUrl); if (fetched.length > 0) return fetched; } catch (e) { log('获取考试列表失败: ' + e.message, 'warn'); } const courseListExams = await this.fetchExamsFromCourseList(); if (courseListExams.length > 0) return courseListExams; return exams.filter(e => !e.url.includes('examSysList') && !isExamListPageUrl(e.url)); }, async getTaskPointsViaAPI() { const { cid, clid, cpi, enc } = getCourseParams(); if (!cid) return null; const apiUrls = [ `https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentstudycourselist?courseId=${cid}&clazzid=${clid}&cpi=${cpi}&mooc2=1&searchChapterListByName=`, `https://mooc1.chaoxing.com/mooc-ans/mycourse/studentstudycourselist?courseId=${cid}&clazzid=${clid}&cpi=${cpi}&mooc2=1`, ]; for (const url of apiUrls) { try { if (!canFetchSameOrigin(url)) continue; const resp = await safeFetch(url, { credentials: 'include' }); if (!resp.ok) continue; const html = await resp.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const ssEl = doc.querySelector('#_studystate, [name="studystate"]'); let studystateData = parseStudystateData(ssEl); if (studystateData?.chapters) { const tasks = []; for (const ch of studystateData.chapters) { if (!ch) continue; const children = ch.children || ch.sections || ch.nodes || []; for (const node of children) { if (node.isTaskPoint && !node.isPassed) { tasks.push({ name: node.name || node.title || '未命名任务', knowledgeId: String(node.knowledgeid || node.knowledgeId || node.id || ''), chapterName: ch.name || ch.title || ch.chapterName || '', chapterId: String(ch.id || ch.chapterId || ''), isDone: false, type: String(node.property || node.type || 0), courseId: cid, clazzid: clid, cpi, enc, }); } const subChildren = node.children || node.sections || node.nodes || []; for (const sub of subChildren) { if (sub.isTaskPoint && !sub.isPassed) { tasks.push({ name: sub.name || sub.title || '未命名任务', knowledgeId: String(sub.knowledgeid || sub.knowledgeId || sub.id || ''), chapterName: node.name || node.title || '', chapterId: String(node.id || node.chapterId || ''), isDone: false, type: String(sub.property || sub.type || 0), courseId: cid, clazzid: clid, cpi, enc, }); } } } if (ch.isTaskPoint && !ch.isPassed && !(ch.children && ch.children.length > 0)) { const chkid = String(ch.knowledgeid || ch.id || ''); if (String(ch.property || ch.type || '') || chkid.length > 5) { tasks.push({ name: ch.name || ch.title || '未命名任务', knowledgeId: chkid, chapterName: ch.chapterName || '', chapterId: String(ch.chapterId || ch.id || ''), isDone: false, type: String(ch.property || ch.type || ''), courseId: cid, clazzid: clid, cpi, enc, }); } } } if (tasks.length > 0) return tasks; } const curItems = doc.querySelectorAll('[id^="cur"]'); const tasks = []; for (const item of curItems) { const subUl = item.querySelector('ul, .posCatalog_level'); if (subUl) continue; const nameEl = item.querySelector('.posCatalog_name, .catalog_name, .clicktitle'); const iconEl = item.querySelector('.posCatalog_icon, [class*="icon"]'); const name = (nameEl ? (nameEl.getAttribute('title') || nameEl.textContent) : item.textContent || '').trim().replace(/\s+/g, ' '); if (!name || name.length < CFG.MIN_TASK_NAME_LEN || name.length > CFG.MAX_TASK_NAME_LEN) continue; const idMatch = (item.id || '').match(/cur(\d+)/); const kid = idMatch ? idMatch[1] : ''; if (!kid) continue; const status = getTaskCompletionStatus(item, iconEl); if (status === 'todo') { const parentChapter = item.closest('.posCatalog_level')?.previousElementSibling; const chapterEl = parentChapter || item.closest('li')?.querySelector('.firstLayer .posCatalog_title'); const chapterName = chapterEl ? (chapterEl.getAttribute('title') || chapterEl.textContent || '').trim().replace(/\s+/g, ' ') : ''; tasks.push(buildTaskCandidate(name, { knowledgeId: kid, chapterName, chapterId: kid, courseId: cid, clazzid: clid, cpi, enc, })); } } if (tasks.length > 0) return tasks; if (studystateData?.unfinishCount !== undefined && studystateData.unfinishCount > 0) { const nextId = String(studystateData.nextChapterId || ''); if (nextId && nextId.length > CFG.MIN_ID_LEN) { return [{ name: '自动检测到未完成任务', knowledgeId: nextId, chapterName: '', chapterId: nextId, isDone: false, type: '', courseId: cid, clazzid: clid, cpi, enc, }]; } return [{ name: '未完成任务(' + studystateData.unfinishCount + '个)', knowledgeId: '', chapterName: '', chapterId: '', isDone: false, type: '', courseId: cid, clazzid: clid, cpi, enc }]; } } catch (_) { /* API请求失败 */ } } return null; }, async getTaskPointsViaDOM() { const tasks = []; const unknownTasks = []; const { cid, clid, cpi } = getCourseParams(); let enc = getUrlParam('enc') || ''; if (!enc) { try { const scripts = document.querySelectorAll('script'); for (const s of scripts) { const match = (s.textContent || '').match(/(?:var|let|const)\s+enc\s*=\s*["']([a-f0-9]{32})["']/); if (match) { enc = match[1]; log('从页面脚本提取enc: ' + enc); break; } } } catch (_) { /* 脚本提取失败 */ } } if (!enc) { try { if (typeof unsafeWindow !== 'undefined' && typeof unsafeWindow.toOld === 'function') { const src = unsafeWindow.toOld.toString(); const match = src.match(/enc\s*=\s*"([a-f0-9]{32})"/); if (match) enc = match[1]; } } catch (_) { /* unsafeWindow访问失败 */ } } const docs = getAllDocs(); for (const doc of docs) { for (const head of doc.querySelectorAll('.chapter_head, .posCatalog_select.chapter, [class*="chapter_item"]')) { if (isVisible(head)) { try { head.click(); } catch (_) { /* 点击失败 */ } } } } await sleep(CFG.CHAPTER_EXPAND_MS); for (const doc of docs) { const items = [...doc.querySelectorAll('[id^="cur"]')]; log(`在文档中找到 ${items.length} 个任务点元素`); for (const item of items) { const subUl = item.querySelector('ul, .posCatalog_level'); if (subUl) continue; const nameEl = item.querySelector('.posCatalog_name, .catalog_name, .clicktitle, a'); const iconEl = item.querySelector('.posCatalog_icon, [class*="icon"]'); const name = (nameEl ? nameEl.textContent : item.textContent || '').trim().replace(/\s+/g, ' '); if (!name || name.length < CFG.MIN_TASK_NAME_LEN || name.length > CFG.MAX_TASK_NAME_LEN) continue; const onclick = item.getAttribute('onclick') || ''; const onclickMatch = onclick.match(/toOld\s*\(\s*'(\d+)'\s*,\s*'(\d+)'/); const kid = onclickMatch ? onclickMatch[2] : (item.id ? item.id.replace('cur', '') : ''); const status = getTaskCompletionStatus(item, iconEl); if (status !== 'done') { const task = buildTaskCandidate(name, { knowledgeId: kid, element: item, clickTarget: nameEl || item, courseId: cid, clazzid: clid, cpi, enc, }); log((status === 'todo' ? '找到未完成任务: ' : '找到状态未知候选任务: ') + name + ' (knowledgeId: ' + kid + ')'); if (status === 'todo') tasks.push(task); else unknownTasks.push(task); } } } const result = tasks.length > 0 ? tasks : unknownTasks; log(`getTaskPointsViaDOM 总共找到 ${result.length} 个候选任务点`); return result; }, getFastTaskPointViaDOM() { const { cid, clid, cpi } = getCourseParams(); let enc = getChaoxingPageEnc(); const preferred = []; const fallback = []; for (const doc of getAllDocs()) { for (const item of doc.querySelectorAll('[id^="cur"]')) { const subUl = item.querySelector('ul, .posCatalog_level'); if (subUl) continue; const nameEl = item.querySelector('.posCatalog_name, .catalog_name, .clicktitle, a'); const iconEl = item.querySelector('.posCatalog_icon, [class*="icon"]'); const name = (nameEl ? nameEl.textContent : item.textContent || '').trim().replace(/\s+/g, ' '); if (!name || name.length < CFG.MIN_TASK_NAME_LEN || name.length > CFG.MAX_TASK_NAME_LEN) continue; const onclick = item.getAttribute('onclick') || ''; const onclickMatch = onclick.match(/toOld\s*\(\s*'(\d+)'\s*,\s*'(\d+)'/); let kid = onclickMatch ? onclickMatch[2] : (item.id ? item.id.replace('cur', '') : ''); if (!kid) { const href = nameEl?.href || item.querySelector('a[href]')?.href || ''; kid = getUrlParamFrom(href, 'chapterId') || getUrlParamFrom(href, 'knowledgeId') || ''; if (!enc) enc = getUrlParamFrom(href, 'enc') || ''; } if (!kid || kid.length <= CFG.MIN_ID_LEN) continue; const task = buildTaskCandidate(name, { knowledgeId: kid, element: item, clickTarget: nameEl || item, courseId: cid, clazzid: clid, cpi, enc, }); const status = getTaskCompletionStatus(item, iconEl); if (status === 'done') fallback.push(task); else preferred.push(task); } } return preferred[0] || fallback[0] || null; }, async getTaskPoints() { let tasks = await this.getTaskPointsViaDOM(); if (!tasks || tasks.length === 0) { tasks = await this.getTaskPointsViaAPI(); } if ((!tasks || tasks.length === 0) && PLATFORM === 'chaoxing') { tasks = this.getTaskPointsFromPageStudystate(); } return tasks; }, getTaskPointsFromPageStudystate() { const { cid, clid, cpi, enc } = getCourseParams(); const docs = getAllDocs(); for (const doc of docs) { const ssEl = doc.querySelector('#_studystate, [name="studystate"]'); if (!ssEl) continue; const data = parseStudystateData(ssEl); if (data?.nextChapterId) { const nextId = String(data.nextChapterId); if (nextId.length > CFG.MIN_ID_LEN) { log('从页面_studystate获取nextChapterId: ' + nextId); return [{ name: '下一个未完成任务', knowledgeId: nextId, chapterName: '', chapterId: '', isDone: false, type: '', courseId: cid, clazzid: clid, cpi, enc, }]; } } } return null; }, pickRandom(tasks) { return pickNextTask(tasks); }, async enterTask(task) { log('开始进入任务: ' + task.name); if (task.knowledgeId && task.knowledgeId.length > CFG.MIN_ID_LEN) { const { cid, clid, cpi, enc } = getCourseParams(task); const url = `https://mooc1.chaoxing.com/mycourse/studentstudy` + `?chapterId=${task.knowledgeId}` + `&courseId=${cid}&clazzid=${clid}&cpi=${cpi}` + `&enc=${enc}&mooc2=1`; log('跳转任务: ' + task.name + ' → ' + url); location.href = url; return; } if (task.clickTarget && task.element) { log('尝试直接点击任务元素'); try { const clickEl = task.element.querySelector('.clicktitle, .catalog_name a, a') || task.element.querySelector('[role="link"]') || task.element; clickEl.scrollIntoView({ block: 'center', behavior: 'smooth' }); await sleep(CFG.ENTER_TASK_SLEEP); task.element.click(); log('直接点击成功!'); return; } catch (e) { log('直接点击失败: ' + e.message); } } if (task.chapterId && task.chapterId.length > CFG.MIN_ID_LEN && task.chapterId !== task.knowledgeId) { const { cid, clid, cpi, enc } = getCourseParams(task); const url = `https://mooc1.chaoxing.com/mycourse/studentstudy` + `?chapterId=${task.chapterId}` + `&courseId=${cid}&clazzid=${clid}&cpi=${cpi}` + `&enc=${enc}&mooc2=1`; log('跳转任务(chapterId): ' + task.name + ' → ' + url); location.href = url; return; } log('任务没有knowledgeId也没有chapterId!尝试从studentstudycourselist API获取...'); try { const { cid, clid, cpi, enc } = getCourseParams(task); const apiUrls = [ `https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentstudycourselist?courseId=${cid}&clazzid=${clid}&cpi=${cpi}&mooc2=1`, `https://mooc1.chaoxing.com/mooc-ans/mycourse/studentstudycourselist?courseId=${cid}&clazzid=${clid}&cpi=${cpi}&mooc2=1`, ]; for (const apiUrl of apiUrls) { if (!canFetchSameOrigin(apiUrl)) continue; const resp = await safeFetch(apiUrl, { credentials: 'include' }); if (!resp.ok) continue; const html = await resp.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const ssEl = doc.querySelector('#_studystate, [name="studystate"]'); const data = parseStudystateData(ssEl); if (data?.nextChapterId) { const url = `https://mooc1.chaoxing.com/mycourse/studentstudy` + `?chapterId=${data.nextChapterId}` + `&courseId=${cid}&clazzid=${clid}&cpi=${cpi}` + `&enc=${enc}&mooc2=1`; log('从API获取nextChapterId跳转: ' + url); location.href = url; return; } } } catch (e) { log('API获取nextChapterId失败: ' + e.message); } }, _clickChapterInIframe(knowledgeId) { const docs = getAllDocs(); for (const doc of docs) { const target = doc.querySelector(`#cur${knowledgeId}`); if (target) { const clickEl = target.querySelector('.clicktitle, .catalog_name a, a, .posCatalog_name') || target.querySelector('[role="link"]') || target; try { clickEl.scrollIntoView({ block: 'center' }); clickEl.click(); log('在iframe中点击章节成功: cur' + knowledgeId); return true; } catch (_) { /* 点击失败 */ } } } return false; }, async checkCompletion() { const cid = getUrlParam('courseId') || getUrlParam('courseid') || getUrlParam('cid'); if (!cid) return this._checkCompletionDOM(); const clid = getUrlParam('clazzid') || getUrlParam('clazzId') || ''; const cpi = getUrlParam('cpi') || ''; const urls = [ `https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentstudycourselist?courseId=${cid}&clazzid=${clid}&cpi=${cpi}&mooc2=1`, `https://mooc1.chaoxing.com/mooc-ans/mycourse/studentstudycourselist?courseId=${cid}&clazzid=${clid}&cpi=${cpi}&mooc2=1`, ]; for (const url of urls) { try { if (!canFetchSameOrigin(url)) continue; const resp = await safeFetch(url, { credentials: 'include' }); if (!resp.ok) continue; const html = await resp.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const ssEl = doc.querySelector('#_studystate, [name="studystate"]'); const data = parseStudystateData(ssEl); if (data) { const total = data.totalNum || 0; const done = data.finishNum || 0; const unfinish = data.unfinishCount; if (total > 0) { return { done, total, percent: Math.round((done / total) * 100), unfinish: unfinish !== undefined ? unfinish : total - done, source: 'api', }; } if (unfinish !== undefined && unfinish > 0 && total === 0) { return { done: 0, total: unfinish, percent: 0, unfinish, remainingOnly: true, source: 'api_state' }; } if (unfinish === 0 && total === 0) { log('API返回totalNum=0且unfinishCount=0,可能是在任务页调用,跳过此次检测'); return null; } } } catch (_) { /* API请求失败 */ } } const pageStudystate = this._checkCompletionFromPageStudystate(); if (pageStudystate) return pageStudystate; return this._checkCompletionDOM(); }, _checkCompletionFromPageStudystate() { const docs = getAllDocs(); for (const doc of docs) { const ssEl = doc.querySelector('#_studystate, [name="studystate"]'); if (!ssEl) continue; const data = parseStudystateData(ssEl); if (data) { const total = data.totalNum || 0; const done = data.finishNum || 0; const unfinish = data.unfinishCount; if (total > 0) { return { done, total, percent: Math.round((done / total) * 100), unfinish: unfinish !== undefined ? unfinish : total - done, source: 'page_state' }; } if (unfinish !== undefined && unfinish > 0) { return { done: 0, total: unfinish, percent: 0, unfinish, remainingOnly: true, source: 'page_state' }; } if (unfinish === 0 && total === 0) { return null; } } } return null; }, _checkCompletionDOM() { const bodyText = document.body?.innerText || ''; const patterns = [ /任务点进度\s*[::]\s*(\d+)\s*\/\s*(\d+)\s*\((\d+)\s*%\)/, /任务点\s*[::]\s*(\d+)\s*\/\s*(\d+)\s*\((\d+)\s*%\)/, /进度\s*[::]\s*(\d+)\s*\/\s*(\d+)\s*\((\d+)\s*%\)/, /进度\s*[::]?\s*(\d+)\s*\/\s*(\d+)\s*\((\d+)\s*%\)/, /完成\s*[::]\s*(\d+)\s*\/\s*(\d+)/, /已完成\s*[::]\s*(\d+)\s*\/\s*(\d+)/, /已完成任务点\s*[::]\s*(\d*)\s*\/\s*(\d+)/, /(\d+)\s*\/\s*(\d+)\s*个任务点/, /任务点:(\d+)\/(\d+)/, /完成:(\d+)\/(\d+)/, /(\d+)\s*\/\s*(\d+)\s*\(\s*(\d+)\s*%\s*\)/, ]; for (const re of patterns) { const m = bodyText.match(re); if (m) { const done = parseInt(m[1], 10) || 0; const total = parseInt(m[2], 10) || 0; if (total > 0) { return { done, total, percent: m[3] ? (parseInt(m[3], 10) || 0) : Math.round((done / total) * 100), source: 'dom' }; } } } for (const iframe of $$('iframe')) { try { const doc = iframe.contentDocument || iframe.contentWindow?.document; if (doc?.body) { const iframeText = doc.body.innerText || ''; for (const re of patterns) { const m = iframeText.match(re); if (m) { const done = parseInt(m[1], 10) || 0; const total = parseInt(m[2], 10) || 0; if (total > 0) { return { done, total, percent: m[3] ? (parseInt(m[3], 10) || 0) : Math.round((done / total) * 100), source: 'iframe' }; } } } } } catch (_) { /* 跨域iframe */ } } return null; }, _textLooksCourseComplete(text) { const keywords = ['全部任务点已完成', '全部任务已完成', '所有任务点已完成', '课程已完成', '本课程已完成', '整门课程已完成', '全部章节已完成']; return keywords.some(kw => text.includes(kw)) || (text.includes('木金') && text.includes('课程') && text.includes('已完成')); }, _textLooksTaskComplete(text) { const keywords = ['当前任务已完成', '页面任务点已完成', '任务已完成', '章节已完成', '章节测试已完成', '随堂测验已完成', '文档已完成', '作业已完成', '任务点已完成']; return keywords.some(kw => text.includes(kw)) && !this._textLooksCourseComplete(text); }, checkCompletionPopup() { const tip = document.getElementById('jobFinishTipFocus'); if (tip && isVisible(tip)) { const text = (tip.parentElement || tip).textContent || ''; if (this._textLooksCourseComplete(text)) return true; } for (const sel of ['.popDiv', '.popContent', '.popBox', '.maskDiv', '.ans-job-finish', '[class*="modal"]', '[class*="dialog"]', '[role="dialog"]']) { for (const el of $$(sel)) { if (!isVisible(el)) continue; const text = el.textContent || ''; if (this._textLooksCourseComplete(text)) return true; } } for (const iframe of $$('iframe')) { try { const doc = iframe.contentDocument || iframe.contentWindow?.document; if (!doc) continue; for (const sel of ['.popDiv', '.popContent', '.popBox', '.maskDiv', '.ans-job-finish', '[class*="modal"]', '[class*="dialog"]']) { for (const el of doc.querySelectorAll(sel)) { const text = el.textContent || ''; if (this._textLooksCourseComplete(text)) return true; } } } catch (_) { /* 跨域iframe */ } } return false; }, checkTaskCompletionPopup() { const tip = document.getElementById('jobFinishTipFocus'); if (tip && isVisible(tip)) { const text = (tip.parentElement || tip).textContent || ''; if (this._textLooksTaskComplete(text)) return true; } for (const doc of getAllDocs()) { for (const sel of ['.popDiv', '.popContent', '.popBox', '.maskDiv', '.ans-job-finish', '[class*="modal"]', '[class*="dialog"]', '[role="dialog"]']) { for (const el of doc.querySelectorAll(sel)) { if (!isVisible(el)) continue; const text = el.textContent || ''; if (this._textLooksTaskComplete(text)) return true; } } } return false; }, async navigateFromCourseDetailToStudy() { const u = location.href; const isCoursePage = u.includes('studentcourse') || u.includes('/mycourse/stu') || u.includes('/visit/interaction'); if (!isCoursePage || u.includes('studentstudy')) return false; const currentTasks = [...document.querySelectorAll('[id^="cur"]')]; if (currentTasks.length > 0) { log('当前页面已有任务点,无需跳转'); return false; } for (const iframe of $$('iframe')) { const src = iframe.src || ''; if (src.includes('studentcourse') && !src.includes('studentstudy')) { log('检测到课程章节iframe,导航进入: ' + src); location.href = src; return true; } if (src.includes('studentstudy') || src.includes('knowledge/cards')) { log('从iframe导航到学习页: ' + src); location.href = src; return true; } } const { cid, clid, cpi, enc } = getCourseParams(); try { const apiUrls = [ `https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentstudycourselist?courseId=${cid}&clazzid=${clid}&cpi=${cpi}&mooc2=1`, `https://mooc1.chaoxing.com/mooc-ans/mycourse/studentstudycourselist?courseId=${cid}&clazzid=${clid}&cpi=${cpi}&mooc2=1`, ]; for (const apiUrl of apiUrls) { try { if (!canFetchSameOrigin(apiUrl)) continue; const resp = await safeFetch(apiUrl, { credentials: 'include' }); if (!resp.ok) continue; const html = await resp.text(); if (html.length < CFG.MIN_HTML_LEN) continue; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const ssEl = doc.querySelector('#_studystate, [name="studystate"]'); const data = parseStudystateData(ssEl); if (data?.nextChapterId) { const url = `https://mooc1.chaoxing.com/mycourse/studentstudy` + `?chapterId=${data.nextChapterId}` + `&courseId=${cid}&clazzid=${clid}&cpi=${cpi}` + `&enc=${enc}&mooc2=1`; log('从API导航到学习页: ' + url); location.href = url; return true; } } catch (_) { /* API请求失败 */ } } } catch (_) { /* API请求失败 */ } const docs = getAllDocs(); for (const doc of docs) { const chapterHeads = doc.querySelectorAll('.chapter_head, [class*="chapter_head"]'); if (chapterHeads.length > 0) { const firstHead = chapterHeads[0]; const toggleEl = firstHead.querySelector('.posCatalog_name, .chapter_name, [title]'); if (toggleEl) { try { toggleEl.click(); } catch (_) { /* 点击失败 */ } } else { try { firstHead.click(); } catch (_) { /* 点击失败 */ } } } } await sleep(CFG.POPUP_MS); for (const doc of docs) { const allItems = [...doc.querySelectorAll('[id^="cur"]')]; for (const item of allItems) { const nameEl = item.querySelector('.posCatalog_name, .catalog_name, .clicktitle, a'); if (!nameEl) continue; const idMatch = (item.id || '').match(/cur(\d+)/); const kid = idMatch ? idMatch[1] : ''; const hasChildren = item.querySelector('ul') !== null; if (!hasChildren && kid) { try { nameEl.click(); } catch (_) { /* 点击失败 */ } await sleep(CFG.NAV_CLICK_SLEEP); const newUrl = location.href; if (newUrl.includes('studentstudy') || newUrl.includes('knowledgeid=')) return true; for (const iframe2 of $$('iframe')) { const src2 = iframe2.src || ''; if (src2.includes('studentstudy') || src2.includes('knowledge/cards')) { location.href = src2; return true; } } break; } } } return false; }, getCourseListUrl() { return 'https://mooc2-ans.chaoxing.com/mooc2-ans/visit/interaction'; }, }; const ZhihuishuAdapter = { parseCourseList() { const courses = [], seen = new Set(); for (const sel of ['a[href*="courseId"]', 'a[href*="recruitId"]', 'a[href*="shareId"]', 'a[href*="/stu/study"]', '.course-item a', '[class*="courseCard"] a', '.lesson-list a']) { for (const el of $$(sel)) { const href = el.href || ''; const text = (el.textContent || '').trim().replace(/\s+/g, ' '); if (href && text && !seen.has(href) && text.length > CFG.MIN_TASK_NAME_LEN && text.length < CFG.MAX_TASK_NAME_LEN) { seen.add(href); courses.push({ name: text, url: href, platform: 'zhihuishu' }); } } } if (courses.length === 0) { for (const el of $$('a')) { const href = el.href || ''; const text = (el.textContent || '').trim().replace(/\s+/g, ' '); if (href && text && !seen.has(href) && text.length > CFG.MIN_TASK_NAME_LEN && text.length < CFG.MAX_TASK_NAME_LEN && (href.includes('study') || href.includes('course'))) { seen.add(href); courses.push({ name: text, url: href, platform: 'zhihuishu' }); } } } return courses; }, async fetchCoursesViaAPI() { return this.parseCourseList(); }, async getTaskPoints() { const tasks = []; for (const el of $$('.chapter-title, .chapterTitle, [class*="chapter"] [class*="title"], .catalogue-item')) { if (isVisible(el)) { try { el.click(); } catch (_) { /* 点击失败 */ } } } await sleep(CFG.CHAPTER_EXPAND_MS); for (const sel of [ '.video-item', '.lesson-item', '.catalogue-item', '[class*="lessonItem"]', 'li[class*="video"]', 'li[class*="lesson"]', '.study-item', ]) { for (const el of $$(sel)) { const nameEl = el.querySelector('.title, .name, span, a') || el; const name = (nameEl.textContent || '').trim().replace(/\s+/g, ' '); if (!name || name.length < CFG.MIN_TASK_NAME_LEN || name.length > CFG.MAX_TASK_NAME_LEN) continue; let isDone = false; const cls = el.className || ''; if (cls.includes('done') || cls.includes('finish') || cls.includes('completed') || cls.includes('studied')) isDone = true; const icon = el.querySelector('.icon-finish, .icon-done, [class*="finish"], [class*="success"], .check-icon'); if (icon && isVisible(icon)) isDone = true; if (!isDone) { tasks.push({ name, knowledgeId: '', chapterName: '', isDone: false, element: el, clickTarget: el.querySelector('a') || el.querySelector('.title, .name') || el, }); } } } return tasks; }, pickRandom(tasks) { return pickNextTask(tasks); }, async enterTask(task) { log('进入智慧树任务: ' + task.name); if (task.clickTarget) { task.clickTarget.scrollIntoView({ block: 'center' }); await sleep(CFG.ENTER_TASK_SLEEP); try { task.clickTarget.click(); } catch (_) { /* 点击失败 */ } } }, async checkCompletion() { const bodyText = document.body?.innerText || ''; const patterns = [ /进度\s*[::]\s*(\d+)\s*\/\s*(\d+)/, /完成\s*[::]\s*(\d+)\s*\/\s*(\d+)/, /已学\s*[::]\s*(\d+)\s*\/\s*(\d+)/, /(\d+)%\s*(完成|已学)/, /学习进度\s*(\d+)%/, /完成度\s*(\d+)%/, ]; for (const re of patterns) { const m = bodyText.match(re); if (m) { if (m[1] && m[2]) { const done = parseInt(m[1], 10) || 0; const total = parseInt(m[2], 10) || 0; if (total > 0) return { done, total, percent: Math.round((done / total) * 100), source: 'dom' }; } else if (m[1]) { const pct = parseInt(m[1], 10) || 0; return { done: pct, total: 100, percent: pct, source: 'dom' }; } } } if (bodyText.includes('100%') && (bodyText.includes('完成') || bodyText.includes('已学'))) { const unfinishedIcons = $$('.icon-unlock, [class*="unlock"], [class*="lock"], [class*="unfinished"]'); if (unfinishedIcons.length === 0) return { done: 100, total: 100, percent: 100, source: 'completion_text' }; } return null; }, checkCompletionPopup() { const text = document.body?.innerText || ''; return (text.includes('课程已完成') || text.includes('全部完成') || text.includes('恭喜你')); }, getCourseListUrl() { return location.origin; }, }; const MoocAdapter = { parseCourseList() { const courses = [], seen = new Set(); for (const sel of ['a[href*="/learn/"]', '.course-card a', '[class*="course"] a[href*="/learn/"]', '.u-course-card a', '.course-list a', 'a[href*="tid="]']) { for (const el of $$(sel)) { const href = el.href || ''; const text = (el.textContent || '').trim().replace(/\s+/g, ' '); if (href && text && !seen.has(href) && text.length > CFG.MIN_TASK_NAME_LEN && text.length < CFG.MAX_TASK_NAME_LEN) { seen.add(href); courses.push({ name: text, url: href, platform: 'mooc' }); } } } return courses; }, async fetchCoursesViaAPI() { return this.parseCourseList(); }, async getTaskPoints() { const tasks = []; for (const el of $$('.chapter, .chaptersection, [class*="chapter"] [class*="header"], .f-icon-arrow-right')) { if (isVisible(el)) { try { el.click(); } catch (_) { /* 点击失败 */ } } } await sleep(CFG.CHAPTER_EXPAND_MS); for (const sel of [ '.j-chapter-section', '.section', '.chapter-item', '[class*="lesson"]', '.u-lesson', '.lesson-item', '[class*="unit"]', ]) { for (const el of $$(sel)) { const linkEl = el.querySelector('a[href*="content"]') || el.querySelector('a') || el; const name = (linkEl.textContent || el.textContent || '').trim().replace(/\s+/g, ' '); if (!name || name.length < CFG.MIN_TASK_NAME_LEN || name.length > CFG.MAX_TASK_NAME_LEN) continue; let isDone = false; const cls = el.className || ''; if (cls.includes('done') || cls.includes('finish') || cls.includes('completed') || cls.includes('studied')) isDone = true; const checkIcon = el.querySelector('.f-icon-correct, .icon-correct, [class*="check"], [class*="finish"]'); if (checkIcon) isDone = true; if (!isDone) { tasks.push({ name, knowledgeId: '', chapterName: '', isDone: false, element: el, clickTarget: linkEl.tagName === 'A' ? linkEl : (el.querySelector('a') || el), }); } } } return tasks; }, pickRandom(tasks) { return pickNextTask(tasks); }, async enterTask(task) { log('进入MOOC任务: ' + task.name); if (task.clickTarget) { task.clickTarget.scrollIntoView({ block: 'center' }); await sleep(CFG.ENTER_TASK_SLEEP); try { task.clickTarget.click(); } catch (_) { /* 点击失败 */ } } }, async checkCompletion() { const bodyText = document.body?.innerText || ''; const patterns = [ /已完成\s*(\d+)\s*\/\s*(\d+)/, /学习进度\s*(\d+)%/, /完成度\s*(\d+)%/, /进度\s*(\d+)\s*\/\s*(\d+)/, ]; for (const re of patterns) { const m = bodyText.match(re); if (m) { if (m[2]) { const done = parseInt(m[1], 10) || 0; const total = parseInt(m[2], 10) || 0; if (total > 0) return { done, total, percent: Math.round((done / total) * 100), source: 'dom' }; } else { const pct = parseInt(m[1], 10) || 0; return { done: pct, total: 100, percent: pct, source: 'dom' }; } } } return null; }, checkCompletionPopup() { const bodyText = document.body?.innerText || ''; return bodyText.includes('课程已完成') || bodyText.includes('恭喜你已完成'); }, getCourseListUrl() { return 'https://www.icourse163.org/'; }, }; const IcveAdapter = { parseCourseList() { const courses = [], seen = new Set(); for (const sel of ['a[href*="courseId"]', 'a[href*="courseid"]', 'a[href*="/course/"]', '.course-item a', '[class*="course"] a', '.course-card a']) { for (const el of $$(sel)) { const href = el.href || ''; const text = (el.textContent || '').trim().replace(/\s+/g, ' '); if (href && text && !seen.has(href) && text.length > CFG.MIN_TASK_NAME_LEN && text.length < CFG.MAX_TASK_NAME_LEN) { seen.add(href); courses.push({ name: text, url: href, platform: 'icve' }); } } } return courses; }, async fetchCoursesViaAPI() { return this.parseCourseList(); }, async getTaskPoints() { const tasks = []; for (const el of $$('.chapter-title, .unit-title, [class*="expand"], [class*="fold"], .tree-node [class*="toggle"]')) { if (isVisible(el)) { try { el.click(); } catch (_) { /* 点击失败 */ } } } await sleep(CFG.CHAPTER_EXPAND_MS); for (const sel of [ '.lesson-item', '.task-item', '[class*="lesson"]', '[class*="taskItem"]', '.study-item', '.learn-item', '[class*="video"]', ]) { for (const el of $$(sel)) { const nameEl = el.querySelector('.title, .name, a, span') || el; const name = (nameEl.textContent || '').trim().replace(/\s+/g, ' '); if (!name || name.length < CFG.MIN_TASK_NAME_LEN || name.length > CFG.MAX_TASK_NAME_LEN) continue; let isDone = false; const cls = el.className || ''; if (cls.includes('done') || cls.includes('finish') || cls.includes('completed') || cls.includes('pass')) isDone = true; const icon = el.querySelector('[class*="finish"], [class*="done"], [class*="success"], [class*="check"]'); if (icon) isDone = true; if (!isDone) { tasks.push({ name, knowledgeId: '', chapterName: '', isDone: false, element: el, clickTarget: el.querySelector('a') || nameEl, }); } } } return tasks; }, pickRandom(tasks) { return pickNextTask(tasks); }, async enterTask(task) { log('进入智慧职教任务: ' + task.name); if (task.clickTarget) { task.clickTarget.scrollIntoView({ block: 'center' }); await sleep(CFG.ENTER_TASK_SLEEP); try { task.clickTarget.click(); } catch (_) { /* 点击失败 */ } } }, async checkCompletion() { const bodyText = document.body?.innerText || ''; const patterns = [ /进度\s*[::]\s*(\d+)\s*\/\s*(\d+)/, /完成\s*[::]\s*(\d+)\s*\/\s*(\d+)/, /已学\s*[::]\s*(\d+)\s*\/\s*(\d+)/, /学习进度\s*(\d+)%/, /完成度\s*(\d+)%/, ]; for (const re of patterns) { const m = bodyText.match(re); if (m) { if (m[2]) { const done = parseInt(m[1], 10) || 0; const total = parseInt(m[2], 10) || 0; if (total > 0) return { done, total, percent: Math.round((done / total) * 100), source: 'dom' }; } else { const pct = parseInt(m[1], 10) || 0; return { done: pct, total: 100, percent: pct, source: 'dom' }; } } } return null; }, checkCompletionPopup() { const bodyText = document.body?.innerText || ''; return /课程已完成|全部课程已完成|全部完成|恭喜(?:你)?(?:已)?完成/.test(bodyText); }, getCourseListUrl() { return location.origin + '/student/course'; }, }; const Adapters = { chaoxing: ChaoxingAdapter, zhihuishu: ZhihuishuAdapter, mooc: MoocAdapter, icve: IcveAdapter }; const Adapter = Adapters[PLATFORM] || null; const Queue = { get() { return loadQueue(); }, save(q) { saveQueue(q); }, add(course) { const q = this.get(); const existing = q.find(c => c.url === course.url); if (existing) { if (existing.completed) { existing.completed = false; delete existing.completedAt; delete existing.skipped; delete existing.skippedAt; delete existing.skipReason; delete existing.skipProgress; existing.addedAt = Date.now(); existing.name = course.name || existing.name; this.save(q); return true; } return false; } if (!existing) { q.push({ ...course, addedAt: Date.now() }); this.save(q); return true; } return false; }, remove(idx) { const q = this.get(); if (idx >= 0 && idx < q.length) { q.splice(idx, 1); this.save(q); return true; } return false; }, move(from, to) { const q = this.get(); if (from < 0 || from >= q.length) return; const targetIdx = Math.max(0, Math.min(to, q.length - 1)); const item = q.splice(from, 1)[0]; q.splice(targetIdx, 0, item); this.save(q); }, clear() { this.save([]); }, getNextPending() { return this.get().find(c => !c.completed) || null; }, pendingCount() { return this.get().filter(c => !c.completed).length; }, markCompleted(url) { const q = this.get(); const c = q.find(x => x.url === url); if (c) { c.completed = true; c.completedAt = Date.now(); delete c.skipped; delete c.skippedAt; delete c.skipReason; delete c.skipProgress; this.save(q); } }, markSkipped(url, reason, prog) { const q = this.get(); const c = q.find(x => x.url === url); if (c) { c.completed = true; c.skipped = true; c.completedAt = Date.now(); c.skippedAt = Date.now(); c.skipReason = reason || '异常跳过'; if (prog) { c.skipProgress = { done: prog.done, total: prog.total, percent: prog.percent, unfinish: prog.unfinish, source: prog.source, }; } this.save(q); } }, progress() { const q = this.get(); const done = q.filter(c => c.completed).length; return { done, total: q.length, pct: q.length > 0 ? Math.round((done / q.length) * 100) : 0 }; }, }; function loadExamState() { try { const r = GM_getValue(storeKey('exam_state'), 'null'); return r ? JSON.parse(r) : null; } catch (_) { return null; } } function saveExamState(s) { try { GM_setValue(storeKey('exam_state'), JSON.stringify(s)); } catch (_) { log('saveExamState 存储异常', 'warn'); } } function clearExamState() { try { GM_deleteValue(storeKey('exam_state')); } catch (_) { /* ignore */ } } const ExamQueue = { get() { try { const r = GM_getValue(storeKey('exam_queue_' + PLATFORM), '[]'); const a = JSON.parse(r); return Array.isArray(a) ? a : []; } catch (_) { return []; } }, save(q) { try { GM_setValue(storeKey('exam_queue_' + PLATFORM), JSON.stringify(q)); } catch (_) { log('saveExamQueue 存储异常', 'warn'); } }, add(exam) { const q = this.get(); const existing = q.find(e => e.url === exam.url); if (existing) { if (existing.completed) { existing.completed = false; delete existing.completedAt; existing.name = exam.name || existing.name; existing.addedAt = Date.now(); this.save(q); return true; } return false; } q.push({ ...exam, addedAt: Date.now() }); this.save(q); return true; }, remove(idx) { const q = this.get(); if (idx >= 0 && idx < q.length) { q.splice(idx, 1); this.save(q); return true; } return false; }, move(from, to) { const q = this.get(); if (from < 0 || from >= q.length) return; const targetIdx = Math.max(0, Math.min(to, q.length - 1)); const item = q.splice(from, 1)[0]; q.splice(targetIdx, 0, item); this.save(q); }, clear() { this.save([]); clearExamState(); }, getNextPending() { return this.get().find(e => !e.completed) || null; }, pendingCount() { return this.get().filter(e => !e.completed).length; }, markCompleted(url) { const q = this.get(); const e = q.find(x => x.url === url); if (e) { e.completed = true; e.completedAt = Date.now(); this.save(q); } }, progress() { const q = this.get(); const done = q.filter(e => e.completed).length; return { done, total: q.length, pct: q.length > 0 ? Math.round((done / q.length) * 100) : 0 }; }, }; const ExamRunner = { openNext() { const next = ExamQueue.getNextPending(); if (!next) { clearExamState(); UI.toast('考试队列已全部处理'); return false; } saveExamState({ running: true, platform: PLATFORM, exam: next, startedAt: Date.now(), }); if (typeof UI !== 'undefined' && UI.createExamBar) UI.createExamBar(next); UI.toast('正在打开考试:' + next.name); location.href = next.url; return true; }, markCurrentCompleted(openNext = true) { const st = loadExamState(); if (!st?.exam) { UI.toast('当前没有正在处理的考试'); return false; } ExamQueue.markCompleted(st.exam.url); clearExamState(); if (typeof UI !== 'undefined' && UI.removeExamBar) UI.removeExamBar(); UI.toast('已标记考试完成:' + st.exam.name); if (openNext) return this.openNext(); return true; }, stop() { clearExamState(); if (typeof UI !== 'undefined' && UI.removeExamBar) UI.removeExamBar(); UI.toast('已停止考试队列'); }, }; const Scheduler = { _monitorTimer: null, _checkRunning: false, _stuckCount: 0, _lastProgress: null, _stagnationCount: 0, _lastApiCheck: 0, _lastTaskRecheck: 0, _lastProgressLog: 0, _lastHeartbeat: 0, _lastUnfinishCount: -1, _apiFailCount: 0, _lastReportedDone: 0, _monitorStart: 0, _initialUrl: '', _lastTaskDoneAt: 0, _lastMeaningfulProgressAt: 0, _reenterCount: 0, start() { const next = Queue.getNextPending(); if (!next) { UI.toast('队列为空!请先添加课程。'); return false; } saveState({ running: true, platform: PLATFORM, course: next, courseIdx: Queue.get().filter(c => !c.completed).indexOf(next), phase: 'navigate', entered: false, startedAt: Date.now(), }); const targetUrl = buildCourseUrl(next.url); log('开始学习: ' + next.name + ' → ' + targetUrl); location.href = targetUrl; return true; }, skip() { const st = loadState(); if (st?.course) Queue.markCompleted(st.course.url); this._stopMonitoring(); this._navigateNext(); }, stop() { this._stopMonitoring(); clearState(); UI.removeStatusBar(); }, _stopMonitoring() { this._checkRunning = false; if (this._monitorTimer) { clearInterval(this._monitorTimer); this._monitorTimer = null; } }, _resetMonitorState() { this._stuckCount = 0; this._lastProgress = null; this._stagnationCount = 0; this._lastApiCheck = 0; this._lastTaskRecheck = 0; this._lastProgressLog = Date.now(); this._lastHeartbeat = Date.now(); this._lastUnfinishCount = -1; this._apiFailCount = 0; this._lastReportedDone = 0; this._monitorStart = Date.now(); this._initialUrl = location.href; this._lastTaskDoneAt = 0; this._lastMeaningfulProgressAt = Date.now(); this._reenterCount = 0; }, _isCourseDoneProgress(prog) { return !!prog && (prog.percent >= 100 || (prog.total > 0 && prog.done >= prog.total)); }, _finishCurrentCourse(st, reason) { if (!st?.course) return false; log(`${reason}: ${st.course.name}`); Queue.markCompleted(st.course.url); this._stopMonitoring(); this._navigateNext(); return true; }, _hasKnownUnfinishedProgress(prog) { if (!prog) return false; if (prog.unfinish !== undefined) return prog.unfinish > 0; if (prog.total > 0) return prog.done < prog.total && prog.percent < 100; return false; }, _skipAbnormalCourse(st, reason, prog) { if (!st?.course) return false; const detail = prog ? ` (${prog.done || 0}/${prog.total || 0}, ${prog.percent || 0}%, 剩余:${prog.unfinish ?? '未知'})` : ''; const finalReason = reason || '异常跳过'; log(`异常跳过课程: ${st.course.name} - ${finalReason}${detail}`, 'warn'); Queue.markSkipped(st.course.url, finalReason, prog); UI.toast('已异常跳过:' + st.course.name); this._stopMonitoring(); this._navigateNext(); return true; }, _shouldAutoSkipCourse(st, now, prog, reason) { if (!st?.course || this._isCourseDoneProgress(prog)) return false; const noProgressMs = now - (this._lastMeaningfulProgressAt || this._monitorStart || now); const hasUnfinished = !prog || this._hasKnownUnfinishedProgress(prog); if (hasUnfinished && noProgressMs >= CFG.MAX_NO_PROGRESS_MS) { return this._skipAbnormalCourse(st, reason || '长时间无进展', prog); } return false; }, async _finishOrSkipWhenNoTask(reason) { const st = loadState(); let prog = null; if (Adapter?.checkCompletion) { try { prog = await Adapter.checkCompletion(); if (prog && st?.course) UI.updateStatusBar(st.course, prog); } catch (_) { /* completion check failed */ } } if (this._isCourseDoneProgress(prog)) { return this._finishCurrentCourse(st, '课程完成'); } if (prog && this._hasKnownUnfinishedProgress(prog)) { return this._skipAbnormalCourse(st, reason || '找不到可进入的未完成任务点', prog); } if (st?.course) Queue.markCompleted(st.course.url); this._stopMonitoring(); this._navigateNext(); return true; }, async _handleTaskCompletionPopup(st, now) { if (!Adapter?.checkTaskCompletionPopup || !Adapter.checkTaskCompletionPopup()) return false; if (now - this._lastTaskDoneAt < CFG.TASK_DONE_REENTER_MS) return false; this._lastTaskDoneAt = now; log('检测到当前任务完成,准备进入下一个未完成任务'); dismissPopups(); if (Adapter?.checkCompletion) { try { const prog = await Adapter.checkCompletion(); if (prog) { UI.updateStatusBar(st.course, prog); this._lastProgress = prog; this._lastUnfinishCount = prog.unfinish !== undefined ? prog.unfinish : -1; this._lastApiCheck = now; this._lastTaskRecheck = now; if (this._isCourseDoneProgress(prog)) { this._finishCurrentCourse(st, '课程完成'); return true; } } } catch (_) { /* 单任务完成后进度检测失败,下次重试 */ } } this._stuckCount = 0; this._stagnationCount = 0; const cur = loadState(); if (cur) { cur.phase = 'navigate'; cur.entered = false; saveState(cur); } await this._enterRandomTask(); return true; }, async _enterRandomTask() { if (!Adapter?.getTaskPoints) return; log('正在查找任务点...'); dismissPopups(); if (PLATFORM === 'chaoxing' && Adapter.switchToNewVersion) { const beforeSwitchUrl = location.href; Adapter.switchToNewVersion(); if (location.href !== beforeSwitchUrl) return; } if (location.href.includes('studentstudy') && !location.href.includes('studentcourse')) { const { cid, clid, cpi } = getCourseParams(); const backUrl = `https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentcourse?courseid=${cid}&clazzid=${clid}&cpi=${cpi}&ut=s&t=${Date.now()}`; log('在任务学习页无法获取任务点,返回课程章节页'); const st = loadState(); if (st) { st.phase = 'navigate'; saveState(st); } location.href = backUrl; return; } if (PLATFORM === 'chaoxing' && Adapter.getFastTaskPointViaDOM) { const fastTask = Adapter.getFastTaskPointViaDOM(); if (fastTask?.knowledgeId && fastTask.courseId && fastTask.clazzid) { log('Fast enter task: ' + fastTask.name); UI.showTaskToast(fastTask.name); const st = loadState(); if (st) { st.entered = true; st.phase = 'click_task'; saveState(st); } await sleep(CFG.ENTER_TASK_SLEEP); await Adapter.enterTask(fastTask); return; } } await sleep(CFG.POPUP_MS); dismissPopups(); clickChapterTab(); await sleep(CFG.CHAPTER_EXPAND_MS); const tasks = await Adapter.getTaskPoints(); if (!tasks || tasks.length === 0) { if (PLATFORM === 'chaoxing' && isChaoxingErrorPage()) { const { cid, clid, cpi, enc } = getCourseParams(); if (cid && clid && location.href.includes('mooc2-ans') && location.href.includes('/visit/stucoursemiddle')) { let fallbackUrl = `https://mooc1.chaoxing.com/visit/stucoursemiddle?courseid=${cid}&clazzid=${clid}`; if (cpi) fallbackUrl += `&cpi=${cpi}`; if (enc) fallbackUrl += `&enc=${enc}`; fallbackUrl += '&ismooc2=1&v=2'; log('检测到 mooc2 课程入口 404,切回有效入口: ' + fallbackUrl); location.href = fallbackUrl; return; } log('当前页面是错误页,暂停任务查找,避免误判课程完成'); UI.toast('当前课程入口页面异常,未标记完成'); return; } log('未找到任务点,可能所有任务已完成'); await this._finishOrSkipWhenNoTask('找不到可进入的未完成任务点'); return; } log(`找到 ${tasks.length} 个未完成任务点`); const picker = Adapter.pickNextTask || Adapter.pickRandom || pickNextTask; const picked = picker.call(Adapter, tasks); if (!picked) { log('所有任务已标记完成'); await this._finishOrSkipWhenNoTask('任务列表没有可选择的未完成任务'); return; } const chapterPath = picked.chapterName || ''; const taskType = TASK_TYPE_MAP[String(picked.type || '')] || ''; const displayParts = []; if (taskType) displayParts.push(`[${taskType}]`); if (chapterPath) displayParts.push(chapterPath); displayParts.push(picked.name); const display = displayParts.join(' > '); log(`选中任务: ${display}`); UI.showTaskToast(display); const st = loadState(); if (st) { st.entered = true; st.phase = 'click_task'; saveState(st); } await sleep(CFG.TASK_DELAY_MS); await Adapter.enterTask(picked); }, _startMonitoring() { this._stopMonitoring(); this._resetMonitorState(); const monitoringStartedAt = Date.now(); let firstCheckDone = false; const check = async () => { if (this._checkRunning) return; this._checkRunning = true; try { const st = loadState(); if (!st?.running) { this._stopMonitoring(); return; } if (checkLoginStatus()) { log('检测到掉线,需要重新登录'); UI.toast('检测到登录过期,请重新登录后继续'); this._stopMonitoring(); return; } const now = Date.now(); const monitorElapsed = now - monitoringStartedAt; if (Adapter?.checkCompletionPopup && Adapter.checkCompletionPopup()) { if (monitorElapsed < CFG.MONITORING_MIN_MS) { log('检测到可能的完成弹窗,但监控运行不足30秒,忽略(可能是页面加载中的临时弹窗)'); } else { log('检测到课程完成弹窗!'); this._finishCurrentCourse(st, '课程完成'); return; } } if (monitorElapsed >= CFG.MONITORING_MIN_MS && await this._handleTaskCompletionPopup(st, now)) { return; } if (Adapter?.checkCompletion) { try { if (now - this._lastApiCheck >= CFG.API_CHECK_INTERVAL_MS) { this._lastApiCheck = now; const prog = await Adapter.checkCompletion(); this._lastTaskRecheck = now; if (prog) { this._apiFailCount = 0; UI.updateStatusBar(st.course, prog); const progressChanged = !this._lastProgress || this._lastProgress.done !== prog.done || (prog.unfinish !== undefined && this._lastUnfinishCount !== prog.unfinish); if (progressChanged) { this._lastMeaningfulProgressAt = now; this._reenterCount = 0; } if (this._lastProgress && this._lastProgress.done === prog.done) { this._stuckCount++; if (prog.unfinish !== undefined && this._lastUnfinishCount === prog.unfinish) { this._stagnationCount++; } else { this._stagnationCount = 0; } } else { this._stuckCount = 0; this._stagnationCount = 0; } this._lastProgress = prog; this._lastUnfinishCount = prog.unfinish !== undefined ? prog.unfinish : -1; if (this._isCourseDoneProgress(prog)) { if (!firstCheckDone && monitorElapsed < CFG.FIRST_CHECK_MIN_MS) { log('进度显示完成但监控运行不足60秒,标记首次检查完成并继续监控'); firstCheckDone = true; } else { this._finishCurrentCourse(st, '课程完成'); return; } } else { firstCheckDone = true; } if (this._shouldAutoSkipCourse(st, now, prog, '连续60分钟无进展,疑似特殊任务无法完成')) { return; } if (this._stagnationCount >= CFG.STAGNATION_MAX) { this._reenterCount++; if (this._shouldAutoSkipCourse(st, now, prog, '连续60分钟无进展,疑似特殊任务无法完成')) return; log('检测到任务长时间停滞,重新选择未完成任务'); this._stagnationCount = 0; this._stuckCount = 0; const cur = loadState(); if (cur) { cur.phase = 'navigate'; cur.entered = false; saveState(cur); } await this._enterRandomTask(); return; } if (this._stuckCount > CFG.STUCK_MAX && prog.unfinish !== undefined && prog.unfinish > 0) { log('进度停滞,尝试重新进入任务点'); this._stuckCount = 0; this._reenterCount++; if (this._shouldAutoSkipCourse(st, now, prog, '连续60分钟无进展,疑似特殊任务无法完成')) return; if (location.href.includes('studentstudy') && !location.href.includes('studentcourse')) { log('当前在任务学习页,先返回课程章节页再重新进入'); const { cid, clid, cpi } = getCourseParams(); const backUrl = `https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentcourse?courseid=${cid}&clazzid=${clid}&cpi=${cpi}&ut=s&t=${Date.now()}`; if (st) { st.phase = 'navigate'; st.entered = false; saveState(st); } location.href = backUrl; return; } await this._enterRandomTask(); return; } if (prog.total > 0 && prog.done - this._lastReportedDone >= CFG.PROGRESS_REPORT_STEP) { log(`进度: ${prog.done}/${prog.total} (${prog.percent}%)`); this._lastReportedDone = prog.done; } } else { this._apiFailCount++; if (this._apiFailCount >= CFG.API_FAIL_MAX) { this._apiFailCount = 0; } if (this._shouldAutoSkipCourse(st, now, null, '连续60分钟无法读取课程进度')) { return; } } } if (now - this._lastTaskRecheck >= CFG.TASK_RECHECK_MS) { this._lastTaskRecheck = now; const prog2 = await Adapter.checkCompletion(); if (this._isCourseDoneProgress(prog2)) { this._finishCurrentCourse(st, '复查确认课程完成'); return; } } } catch (_) { /* 检测异常,下次重试 */ } } if (now - this._lastProgressLog >= CFG.PROGRESS_LOG_MS) { this._lastProgressLog = now; const prog = this._lastProgress; if (prog && prog.total > 0) { log(`📊 [${st.course.name}] 进度: ${prog.done}/${prog.total} (${prog.percent}%) [来源:${prog.source}]`); } else { log(`📊 [${st.course.name}] 等待木金脚本运行中...`); } } if (now - this._lastHeartbeat >= CFG.HEARTBEAT_MS) { this._lastHeartbeat = now; const elapsed = (now - this._monitorStart) / 60000; log(`💓 学习心跳: ${st.course.name},已运行: ${formatMinutes(elapsed)}`); } } finally { this._checkRunning = false; } }; setTimeout(() => { check(); }, CFG.PAGE_LOAD_MS); this._monitorTimer = setInterval(check, CFG.MONITOR_MS); }, _navigateNext() { const next = Queue.getNextPending(); if (!next) { const st = loadState(); if (st) { const totalMs = Date.now() - (st.startedAt || Date.now()); const mins = totalMs / 60000; clearState(); UI.showAllDone(formatMinutes(mins)); try { GM_notification({ title: '木金课程队列', text: '全部课程已完成!', timeout: CFG.NOTIFY_TIMEOUT }); } catch (_) { /* 通知失败 */ } } return; } const st = loadState(); const courseIdx = (st ? st.courseIdx || 0 : 0) + 1; saveState({ running: true, platform: PLATFORM, course: next, courseIdx: courseIdx, phase: 'navigate', entered: false, startedAt: st ? st.startedAt : Date.now(), }); log(`切换到: ${next.name} (第${courseIdx + 1}门)`); UI.showTransition(next, () => { location.href = buildCourseUrl(next.url); }, () => { log('已暂停自动切课'); }); }, async onPageLoad() { const st = loadState(); if (!st?.running) return; const pageType = PageDetector.detect(); log(`页面类型: ${pageType}, 阶段: ${st.phase}`); if (pageType === 'courseDetail') { UI.createStatusBar(st.course, null); if (PLATFORM === 'chaoxing' && Adapter?.switchToNewVersion) { Adapter.switchToNewVersion(); await sleep(CFG.POPUP_MS); } dismissPopups(); await sleep(CFG.PAGE_LOAD_MS); dismissPopups(); clickChapterTab(); await sleep(CFG.CHAPTER_EXPAND_MS); if (PLATFORM === 'chaoxing' && Adapter?.navigateFromCourseDetailToStudy) { const navigated = await Adapter.navigateFromCourseDetailToStudy(); if (navigated) return; } if (st.phase === 'navigate' || !st.entered) { await this._enterRandomTask(); } } else if (pageType === 'taskPage') { UI.createStatusBar(st.course, null); dismissPopups(); if (st.phase === 'click_task' || st.entered) { await sleep(CFG.PAGE_LOAD_MS); dismissPopups(); st.phase = 'monitor'; saveState(st); this._startMonitoring(); } else if (st.phase === 'navigate' || !st.entered) { await sleep(CFG.PAGE_LOAD_MS); dismissPopups(); st.phase = 'monitor'; st.entered = true; saveState(st); this._startMonitoring(); } } else if (pageType === 'courseList') { if (st.course) { log('回到课程列表,继续进入当前队列课程'); await sleep(CFG.SWITCH_MS); location.href = buildCourseUrl(st.course.url); } } else { UI.createStatusBar(st.course, null); if (st.phase === 'navigate' || !st.entered) { log('页面类型未知但正在运行,等待后重试...'); await sleep(CFG.PAGE_LOAD_MS); await this._enterRandomTask(); } } }, }; const UI = { _panelEl: null, _statusBarEl: null, _examBarEl: null, _transitionEl: null, _courseListEl: null, _fetchedCourses: [], _fetchedExams: [], _courseCheckboxes: [], _dragCleanup: null, _cleanupAllPanels() { try { if (this._dragCleanup) { this._dragCleanup(); this._dragCleanup = null; } for (const doc of getAllUiDocs()) { doc.querySelectorAll('.mjn3-panel, #mjn3-panel, [data-mjn3-ui="panel"]').forEach(el => el.remove()); doc.querySelectorAll('.mjn3-bar, #mjn3-bar, #mjn3-status-bar, [data-mjn3-ui="status"]').forEach(el => el.remove()); doc.querySelectorAll('.mjn3-overlay, #mjn3-transition-overlay, [data-mjn3-ui="overlay"]').forEach(el => el.remove()); doc.querySelectorAll('.mjn3-toast, .mjn3-task-toast, [data-mjn3-ui="toast"]').forEach(el => el.remove()); } } catch (_) { /* 清理异常 */ } }, injectStyles() { GM_addStyle(` .mjn3-panel { position:fixed;top:80px;right:20px;z-index:999999;width:390px;max-width:calc(100vw - 24px); background:#fff;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.15); overflow:hidden;font-family:"Microsoft YaHei","PingFang SC",sans-serif;font-size:14px; pointer-events:auto !important;user-select:auto !important; } .mjn3-panel * { box-sizing:border-box;pointer-events:auto !important;user-select:auto !important; } .mjn3-header { background:linear-gradient(135deg,#6366F1,#8B5CF6);color:#fff; padding:12px 14px;display:flex;align-items:center;justify-content:space-between;gap:10px; cursor:move;user-select:none; } .mjn3-header h3 { margin:0;font-size:15px;font-weight:700;pointer-events:none;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } .mjn3-header-actions { display:flex;align-items:center;gap:6px;flex-shrink:0; } .mjn3-home-btn { height:28px;border:none;border-radius:6px;background:rgba(255,255,255,.22); color:#fff;font-size:12px;font-weight:600;cursor:pointer;padding:0 8px; white-space:nowrap;transition:background .15s; } .mjn3-home-btn:hover { background:rgba(255,255,255,.36); } .mjn3-collapse-btn { width:28px;height:28px;border:none;border-radius:6px;background:rgba(255,255,255,.2); color:#fff;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center; transition:background .15s;flex-shrink:0;margin-left:8px; } .mjn3-collapse-btn:hover { background:rgba(255,255,255,.35); } .mjn3-panel.collapsed .mjn3-section, .mjn3-panel.collapsed .mjn3-footer { display:none; } .mjn3-panel.collapsed { width:auto;min-width:200px; } .mjn3-btn-stop { flex:1;padding:10px 16px;border:none;border-radius:8px; background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff; font-size:14px;font-weight:600;cursor:pointer;transition:opacity .2s; } .mjn3-btn-stop:hover { opacity:.9; } .mjn3-badge { background:rgba(255,255,255,.2);border-radius:20px;padding:2px 12px;font-size:12px; } .mjn3-section { border-bottom:1px solid #f0f0f0; } .mjn3-section-header { padding:10px 16px;display:flex;align-items:center;justify-content:space-between; background:#fafbff;font-weight:600;font-size:13px;color:#555; } .mjn3-section-body { max-height:200px;overflow-y:auto;padding:4px 8px; } .mjn3-section-body.empty { padding:16px;text-align:center;color:#999;font-size:13px; } .mjn3-course-item { display:flex;align-items:center;padding:6px 8px;border-radius:6px; margin-bottom:2px;transition:background .15s;cursor:pointer; } .mjn3-course-item:hover { background:#f0f2ff; } .mjn3-course-item input[type="checkbox"] { appearance:none !important;-webkit-appearance:none !important;box-sizing:border-box !important; margin:0 8px 0 0;border:2px solid #a5b4fc;border-radius:4px;background:#fff; width:16px !important;height:16px !important;flex-shrink:0; display:inline-block !important;opacity:1 !important;visibility:visible !important; position:relative !important;pointer-events:auto !important;cursor:pointer !important; } .mjn3-course-item input[type="checkbox"]:checked { background:#6366F1;border-color:#6366F1; } .mjn3-course-item input[type="checkbox"]:checked::after { content:'';position:absolute;left:3px;top:0;width:5px;height:9px; border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg); } .mjn3-course-item label { flex:1;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; cursor:pointer; } .mjn3-course-tag { margin-left:6px;font-size:10px;color:#22c55e;white-space:nowrap;flex-shrink:0; } .mjn3-btn-row { display:flex;gap:6px;padding:6px 12px;flex-wrap:wrap; } .mjn3-sbtn { padding:5px 12px;border:1px solid #ddd;border-radius:6px; background:#fff;color:#555;font-size:12px;cursor:pointer;transition:.15s; } .mjn3-sbtn:hover { border-color:#6366F1;color:#6366F1; } .mjn3-sbtn:disabled { opacity:.5;cursor:not-allowed; } .mjn3-sbtn.primary { background:#6366F1;color:#fff;border-color:#6366F1; } .mjn3-sbtn.primary:hover { background:#5558e6; } .mjn3-sbtn.danger { background:#fee2e2;color:#dc2626;border-color:#fecaca; } .mjn3-sbtn.danger:hover { background:#fecaca; } .mjn3-item { display:flex;align-items:center;padding:8px 12px;border-radius:8px; margin-bottom:3px;background:#f8f9ff;transition:background .2s; } .mjn3-item:hover { background:#eef0ff; } .mjn3-item.done { background:#f0fdf4;opacity:.8; } .mjn3-item.skipped { background:#fff7ed;opacity:.85; } .mjn3-idx { width:24px;height:24px;border-radius:50%;background:#6366F1;color:#fff; font-size:11px;display:flex;align-items:center;justify-content:center; margin-right:8px;flex-shrink:0; } .mjn3-item.done .mjn3-idx { background:#22c55e; } .mjn3-item.skipped .mjn3-idx { background:#f59e0b; } .mjn3-info { flex:1;min-width:0; } .mjn3-name { font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; } .mjn3-meta { font-size:10px;color:#888;margin-top:1px; } .mjn3-btn { width:24px;height:24px;border:none;border-radius:5px;cursor:pointer; font-size:12px;display:flex;align-items:center;justify-content:center;transition:.15s; } .mjn3-btn-rm { background:#fee2e2;color:#dc2626; } .mjn3-btn-rm:hover { background:#fecaca; } .mjn3-btn-up,.mjn3-btn-down { background:#e0e7ff;color:#6366F1; } .mjn3-btn-up:hover,.mjn3-btn-down:hover { background:#c7d2fe; } .mjn3-footer { padding:10px 12px;border-top:1px solid #f0f0f0;display:flex;gap:8px; } .mjn3-btn-start { flex:1;padding:10px 16px;border:none;border-radius:8px; background:linear-gradient(135deg,#6366F1,#8B5CF6);color:#fff; font-size:14px;font-weight:600;cursor:pointer;transition:opacity .2s; } .mjn3-btn-start:hover { opacity:.9; } .mjn3-btn-start:disabled { opacity:.5;cursor:not-allowed; } .mjn3-btn-sec { padding:10px 16px;border:1px solid #ddd;border-radius:8px; background:#fff;color:#666;font-size:13px;cursor:pointer;transition:.2s; } .mjn3-btn-sec:hover { border-color:#999;color:#333; } .mjn3-bar { position:fixed;bottom:0;left:0;right:0;z-index:999999; background:linear-gradient(135deg,#6366F1,#8B5CF6);color:#fff; padding:10px 20px;display:flex;align-items:center;justify-content:space-between; font-family:"Microsoft YaHei","PingFang SC",sans-serif;font-size:13px; } .mjn3-bar .mjn3-pbar { width:150px;height:6px;background:rgba(255,255,255,.25);border-radius:3px;overflow:hidden; } .mjn3-bar .mjn3-pfill { height:100%;background:#fff;border-radius:3px;transition:width .3s; } .mjn3-bar .mjn3-skip { background:rgba(255,255,255,.2);color:#fff;border:1px solid rgba(255,255,255,.3); padding:4px 12px;border-radius:6px;font-size:12px;cursor:pointer; } .mjn3-bar .mjn3-skip:hover { background:rgba(255,255,255,.35); } .mjn3-overlay { position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,.5); display:flex;align-items:center;justify-content:center; font-family:"Microsoft YaHei","PingFang SC",sans-serif; } .mjn3-dialog { background:#fff;border-radius:16px;padding:32px;width:420px;text-align:center; box-shadow:0 16px 48px rgba(0,0,0,.2); } .mjn3-dialog .mjn3-icon { width:64px;height:64px;border-radius:50%; background:linear-gradient(135deg,#22c55e,#16a34a); display:flex;align-items:center;justify-content:center; margin:0 auto 20px;font-size:32px;color:#fff; } .mjn3-dialog .mjn3-icon.warn { background:linear-gradient(135deg,#f59e0b,#d97706); } .mjn3-dialog h2 { margin:0 0 8px;font-size:20px;color:#333; } .mjn3-dialog .mjn3-course { color:#6366F1;font-weight:600; } .mjn3-dialog .mjn3-sub { font-size:14px;color:#888;margin-bottom:16px; } .mjn3-dialog .mjn3-cd { font-size:13px;color:#999;margin-bottom:20px; } .mjn3-dialog .mjn3-cd span { font-weight:700;color:#6366F1; } .mjn3-dialog .mjn3-btns { display:flex;gap:12px;justify-content:center; } .mjn3-dialog .mjn3-btns .mjn3-btn-start { flex:0 1 auto;min-width:110px; } .mjn3-toast { position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:99999999; background:#333;color:#fff;padding:10px 24px;border-radius:8px; font-size:14px;font-family:"Microsoft YaHei",sans-serif; box-shadow:0 4px 16px rgba(0,0,0,.2); } .mjn3-task-toast { position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:99999999; background:rgba(0,0,0,.8);color:#fff;padding:16px 32px;border-radius:12px; font-size:16px;font-family:"Microsoft YaHei",sans-serif;text-align:center; box-shadow:0 8px 32px rgba(0,0,0,.3);transition:opacity .3s; } .mjn3-loading { display:inline-block;width:14px;height:14px;border:2px solid rgba(99,102,241,.3); border-top-color:#6366F1;border-radius:50%;animation:mjn3-spin .6s linear infinite; margin-right:6px;vertical-align:middle; } @keyframes mjn3-spin { to { transform:rotate(360deg); } } `); }, escapeHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }, createPanel() { this._cleanupAllPanels(); this._panelEl = null; const q = Queue.get(); const prog = Queue.progress(); const pendingCount = q.filter(c => !c.completed).length; const st = loadState(); const isRunning = st?.running; const examQ = ExamQueue.get(); const examProg = ExamQueue.progress(); const examPendingCount = examQ.filter(e => !e.completed).length; const examSt = loadExamState(); const isExamRunning = examSt?.running; const panel = document.createElement('div'); panel.className = 'mjn3-panel'; panel.id = 'mjn3-panel'; let courseListHtml = ''; if (this._fetchedCourses.length === 0) { courseListHtml = `