// ==UserScript== // @name 麦课刷课助手(视频+图文+极速模式) // @namespace http://tampermonkey.net/ // @version 0.1.1 // @description 视频主动触发结束,支持多播放器(XGPlayer/VideoJS/DPlayer/ArtPlayer),自动翻页,浮动控制面板,兼容验证码页,图文页面优化,统计信息,快捷键支持,page-WH page-video页面支持。已移除 Bootstrap 依赖,完全使用 TDesign(Vue/TDesign 由脚本内动态加载,杜绝 CDN 时序导致的 ref 崩溃)。🚀 新增极速模式:直接调用接口完成课程(15-25秒/课,风险较高,谨慎使用) // @author Ethan // @match *://weiban.mycourse.cn/* // @match *://*.mycourse.cn/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_deleteValue // @connect api.mk.heymeta.cn // @connect mk.heymeta.cn // @connect weiban.mycourse.cn // @connect *.mycourse.cn // @run-at document-start // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.slim.min.js // @require https://cdn.jsdelivr.net/npm/ajax-hook@2.1.3/dist/ajaxhook.min.js // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2.2.1/dist/index.js // ==/UserScript== (function() { 'use strict'; // ========== wkzy 图文/答题页 item.js 兼容:保证页面 $ 可用 ========== // wkzy 图文课件页(/9080/.../item.html + js/item.js)的 item.js 依赖全局 $(jQuery), // 当页面自身 jQuery 未加载/加载滞后时会报 "Uncaught ReferenceError: $ is not defined", // 导致整页答题/翻页交互失效。此处把脚本已通过 @require 加载的 jQuery 暴露到页面主世界; // 若沙箱内 jQuery 不可用,则用 CDN 同步注入兜底(document.write 阻塞解析,保证 item.js 之前就绪)。 try { var _skPw = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; if (_skPw && /wkzy\.mycourse\.cn$/i.test(location.hostname) && !_skPw.jQuery) { if (window.jQuery) { _skPw.$ = _skPw.jQuery = window.jQuery; } else { document.write('<\/scr' + 'ipt>'); } } } catch (_) {} // ========== 极早期跨子域桥监听(不依赖任何库/CDN,立即就绪) ========== // weiban.mycourse.cn 与 mcwk.mycourse.cn 是不同 origin,localStorage 互不可见。 // 本监听在脚本一加载就注册,让兄弟子域的隐藏 iframe 能立刻拿到本域快照。 try { if (!window.__sk_bridge_early) { window.__sk_bridge_early = true; window.addEventListener('message', function (e) { try { if (!e.data || typeof e.data !== 'object' || e.data.__sk_bridge_req !== true) return; var snap = { __sk_bridge_resp: true, bank: null, category: null, exam: null, project: null, course: null, user: null }; // 题库(平铺 key,跨域可桥) try { var raw = localStorage.getItem('shuake_question_bank_v1'); var recs = raw ? JSON.parse(raw) : []; if (Array.isArray(recs) && recs.length > 0) snap.bank = recs.slice(0, 2000); } catch (_) {} // 面板数据(identity 维度:userProjectId/userId/tenantCode 哈希,与 DataCache 一致) try { var p = new URLSearchParams(window.location.search); var hp = null; var h = window.location.hash; if (h && h.indexOf('userProjectId=') >= 0) { try { hp = new URLSearchParams(h.split('?')[1] || ''); } catch (_) {} } var up = (hp && hp.get('userProjectId')) || p.get('userProjectId') || ''; var uid = (hp && hp.get('userId')) || p.get('userId') || ''; var tc = (hp && hp.get('tenantCode')) || p.get('tenantCode') || ''; var base = (up || uid || tc || ''); var dim = (base || (window.location.hostname || 'anon')); var hh = 0; for (var i = 0; i < dim.length; i++) { var c = dim.charCodeAt(i); hh = ((hh << 5) - hh) + c; hh = hh & hh; } var id = hh.toString(36); ['category', 'exam', 'project', 'course'].forEach(function (t) { try { var r = localStorage.getItem('shuake_data_v1_' + t + '_' + id); if (r) { var obj = JSON.parse(r); if (obj && obj.v != null) snap[t] = obj.v; } } catch (_) {} }); } catch (_) {} // 登录态:本域 localStorage['user'](含 token/tenantCode/userId),跨域桥给兄弟域 try { var uRaw = localStorage.getItem('user'); if (uRaw) { var uObj = JSON.parse(uRaw); if (uObj && typeof uObj === 'object' && (uObj.token || uObj.jwtToken)) snap.user = uObj; } } catch (_) {} if (e.source && typeof e.source.postMessage === 'function') { e.source.postMessage(snap, '*'); } } catch (_) {} }); } } catch (_) {} // ========== 早期启动提示 ========== console.log('%c[麦课助手] 脚本开始加载...', 'color: #00ff00; font-weight: bold; font-size: 16px'); console.log('[麦课助手] URL:', window.location.href); console.log('[麦课助手] 时间:', new Date().toLocaleString()); console.log('[麦课助手] 正在等待依赖库加载(Vue、TDesign)...'); console.log('[麦课助手] 🔍 初始库状态:', { jQuery: typeof window.jQuery, ah: typeof window.ah, VM: typeof window.VM, Vue: typeof window.Vue, TDesign: typeof window.TDesign }); // 手动加载 TDesign CSS (function loadTDesignCSS() { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdn.jsdelivr.net/npm/tdesign-vue-next@1.10.6/dist/tdesign.min.css'; link.onload = () => { console.log('[麦课助手] TDesign CSS 加载完成'); console.log('[麦课助手] 📦 CSS加载后库状态:', { jQuery: typeof window.jQuery, Vue: typeof window.Vue, TDesign: typeof window.TDesign }); }; link.onerror = () => console.error('[麦课助手] TDesign CSS 加载失败'); (document.head || document.documentElement).appendChild(link); })(); // 创建早期页面提示 function createEarlyNotice() { try { const notice = document.createElement('div'); notice.id = 'shuake-early-notice'; notice.style.cssText = ` position: fixed; top: 24px; right: 24px; background: #fff; color: rgba(0, 0, 0, 0.9); padding: 12px 16px; border-radius: 3px; font-size: 14px; line-height: 22px; z-index: 6000; box-shadow: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1); opacity: 0; transition: opacity 0.2s cubic-bezier(0.38, 0, 0.24, 1); display: flex; align-items: center; gap: 8px; `; notice.innerHTML = ` 正在加载依赖库... `; if (document.body) { document.body.appendChild(notice); setTimeout(() => notice.style.opacity = '1', 100); } else { const observer = new MutationObserver(() => { if (document.body) { document.body.appendChild(notice); setTimeout(() => notice.style.opacity = '1', 100); observer.disconnect(); } }); observer.observe(document.documentElement, { childList: true }); } } catch (e) { console.warn('[麦课助手] 创建早期提示失败:', e); } } // 延迟创建提示,确保 DOM 准备好 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createEarlyNotice); } else { createEarlyNotice(); } // ========== 全局错误捕获 ========== // 防止单个异常导致整个脚本崩溃 function safeCall(fn, context = null, ...args) { try { return fn.apply(context, args); } catch (e) { console.debug(`[shuake] safeCall 异常: ${e?.message || e}`); return undefined; } } function safeAsync(fn, context = null, ...args) { return Promise.resolve() .then(() => fn.apply(context, args)) .catch(e => { console.debug(`[shuake] safeAsync 异常: ${e?.message || e}`); return undefined; }); } // 包装 setInterval / setTimeout,统一捕获异常 function safeInterval(fn, delay) { return setInterval(() => safeCall(fn), delay); } function safeTimeout(fn, delay) { return setTimeout(() => safeCall(fn), delay); } // 安装全局错误监听(最早安装) window.addEventListener('error', (e) => { try { const msg = e?.message || String(e?.error || e); const src = e?.filename ? ` @${e.filename}:${e.lineno}` : ''; // 特殊处理 Vue ref 错误 if (msg.includes("reading 'ref'") || msg.includes("reading 'reactive'")) { console.warn('[shuake] ⚠️ Vue 库尚未完全加载,等待中...'); console.warn('[shuake] 错误详情:', msg + src); e.preventDefault(); // 阻止错误冒泡 return true; } // 特殊处理 Logger TDZ 错误 if (msg.includes("Cannot access 'Logger' before initialization")) { console.warn('[shuake] ⚠️ Logger 初始化顺序问题,已忽略'); e.preventDefault(); return true; } console.warn(`[shuake] 全局错误捕获: ${msg}${src}`); // 通过全局对象间接访问 Logger,避免 TDZ try { const L = window.__shuake?.Logger; if (L && typeof L.error === 'function') { L.error(`全局错误: ${msg}${src}`); } } catch (_) { console.error('[shuake] global error:', msg, src); } } catch (_) { /* 静默 */ } e.preventDefault?.(); }); window.addEventListener('unhandledrejection', (e) => { try { const reason = e?.reason; const msg = (reason && (reason.message || reason.toString())) || 'unknown'; // 特殊处理 Logger TDZ 错误 if (msg.includes("Cannot access 'Logger' before initialization")) { console.warn('[shuake] ⚠️ Logger 初始化顺序问题(Promise),已忽略'); e.preventDefault(); return true; } // 过滤浏览器自动播放拦截(站点自身 play() 未捕获的拒绝,无害但刷屏): // 此时只 console.debug,不再打印 error,避免日志被"play() failed"刷满 if (/play\(\) failed|NotAllowedError|autoplay|user activation|user gesture/i.test(msg)) { try { console.debug('[shuake] 站点自动播放被拦截(已忽略): ' + msg); } catch (_) {} e.preventDefault?.(); return true; } // 通过全局对象间接访问 Logger,避免 TDZ try { const L = window.__shuake?.Logger; if (L && typeof L.error === 'function') { L.error(`未处理 Promise 异常: ${msg}`); } else { console.error('[shuake] unhandled rejection:', msg); } } catch (_) { console.error('[shuake] unhandled rejection:', msg); } } catch (_) { /* 静默 */ } e.preventDefault?.(); }); // ========== 安全 JSON 解析 ========== const SafeJSON = { parse(text, fallback = null) { if (text == null) return fallback; if (typeof text !== 'string') return text; // 非字符串:直接原样返回(兼容性) const trimmed = text.trim(); // 空字符串 / 空白 → 视为无内容,不打"失败"日志(404/重定向常返回空 body) if (trimmed.length === 0) return fallback; // 单一 JSON 值不会以 '<' 开头;大概率是 HTML 错误页:静默返回 fallback if (trimmed.startsWith('<') || trimmed.startsWith('/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // ========== 第三方库加载检测 ========== // 包装对 jQuery / ajax-hook / @violentmonkey/dom / Vue / TDesign 的访问 // 这些库通过 @require 由脚本管理器加载,但加载可能因 CDN 故障而失败 // 此处提供统一的检测与降级,确保脚本在库未加载时仍能工作 const Libs = { // 内部就绪状态 _ready: false, _jquery: null, _ajaxhook: null, _vmdom: null, _vue: null, _tdesign: null, init() { try { this._jquery = (typeof window !== 'undefined' && window.jQuery) ? window.jQuery : null; this._ajaxhook = (typeof window !== 'undefined' && window.ah) ? window.ah : null; this._vmdom = (typeof window !== 'undefined' && window.VM) ? window.VM : null; // 优先使用稳定引用(防止 window.Vue/TDesign 被页面代码覆盖) this._vue = _sandboxVueRef || ((typeof window !== 'undefined' && window.Vue) ? window.Vue : null); this._tdesign = _sandboxTDRef || ((typeof window !== 'undefined' && window.TDesign) ? window.TDesign : null); // 如果稳定引用存在但 window 上的丢失了,恢复它(确保 TDesign 组件能访问) if (this._vue && typeof window !== 'undefined' && !window.Vue) { window.Vue = this._vue; console.log('[shuake] 🔄 window.Vue 已从稳定引用恢复'); } if (this._tdesign && typeof window !== 'undefined' && !window.TDesign) { window.TDesign = this._tdesign; console.log('[shuake] 🔄 window.TDesign 已从稳定引用恢复'); } this._ready = true; // 【课程名跨域存储】weiban 域课程详情页 URL 带 courseName(#/course/detail?courseName=...), // 而图文/视频课件页(mcwk 域 iframe)URL 不携带。此处解析并存到 GM 存储, // 课件页上报(StudyReporter.report 兜底)读取同 key,保证 completed 记录课程名不落空。 try { let _cn = ''; try { const _h = (typeof location !== 'undefined' && location.hash) || ''; const _qi = _h.indexOf('?'); if (_qi >= 0) _cn = new URLSearchParams(_h.slice(_qi + 1)).get('courseName') || ''; } catch (_) {} if (!_cn) { try { _cn = new URLSearchParams((typeof location !== 'undefined' && location.search) || '').get('courseName') || ''; } catch (_) {} } if (_cn) { try { _cn = decodeURIComponent(_cn); } catch (_) {} if (_cn && _cn !== 'undefined') { try { if (typeof GM_setValue === 'function') GM_setValue('sk_course_name', _cn); } catch (_) {} try { State.currentCourseName = _cn; } catch (_) {} } } } catch (_) {} // 【课程名·iframe src 兜底】宿主页 URL 无 courseName 参数时,从 page-iframe src 解析 courseId, // 调站点 listCourse.do(GM_xmlhttpRequest 跨域)匹配课程名 → GM 存储,供课件页上报兜底读取。 if (!_cn) { try { let _cid = ''; try { const _if = document.querySelector('iframe.page-iframe, iframe[src*="mcwk"], iframe[src*="course/"]'); if (_if && _if.src) { try { _cid = new URL(_if.src).searchParams.get('courseId') || ''; } catch (_) {} } } catch (_) {} if (_cid) { (async () => { try { if (typeof StudyAssistant !== 'undefined' && typeof StudyAssistant.fetchCourses === 'function') { const _list = await StudyAssistant.fetchCourses(); const _m = (Array.isArray(_list) ? _list : []).find((c) => (c.resourceId && c.resourceId === _cid) || (c.courseId && c.courseId === _cid)); if (_m && _m.resourceName) { try { if (typeof GM_setValue === 'function') GM_setValue('sk_course_name', _m.resourceName); } catch (_) {} try { State.currentCourseName = _m.resourceName; } catch (_) {} } } } catch (_) {} })(); } } catch (_) {} } const available = { jquery: !!this._jquery, ajaxhook: !!this._ajaxhook, vmdom: !!this._vmdom, vue: !!this._vue, tdesign: !!this._tdesign }; console.log('[shuake] 🔍 Library loading status:', available); // 详细检测 Vue API if (this._vue) { console.log('[shuake] ✅ Vue detected:', { hasRef: typeof this._vue?.ref === 'function', hasReactive: typeof this._vue?.reactive === 'function', hasCreateApp: typeof this._vue?.createApp === 'function' }); } else { console.warn('[shuake] ⚠️ Vue is not available on window.Vue'); } // 注意:直接引用 `Logger` 在 const TDZ 期间会抛 ReferenceError // 即使 `typeof Logger` 在 TDZ 也会抛错,必须通过 window.__shuake.Logger 间接访问 try { const L = (typeof window !== 'undefined' && window.__shuake && window.__shuake.Logger) || null; if (L && typeof L.info === 'function') { L.info(`第三方库加载状态: ${JSON.stringify(available)}`); } } catch (_) { /* Logger 尚未初始化时静默 */ } } catch (e) { console.error('[Libs] 检测失败:', e); } }, get $() { // 实时检测,不依赖缓存 if (!this._jquery && typeof window !== 'undefined' && window.jQuery) { this._jquery = window.jQuery; } return this._jquery || null; }, get ah() { if (!this._ajaxhook && typeof window !== 'undefined' && window.ah) { this._ajaxhook = window.ah; } return this._ajaxhook || null; }, get VM() { if (!this._vmdom && typeof window !== 'undefined' && window.VM) { this._vmdom = window.VM; } return this._vmdom || null; }, get Vue() { // 实时检测 Vue,不依赖初始化时的缓存 // 防御:如果缓存失效,尝试从 window 重新获取 if (!this._vue && typeof window !== 'undefined' && window.Vue) { this._vue = window.Vue; console.log('[shuake] ✅ Vue loaded dynamically'); } // 二次防御:如果 window.Vue 也丢失了,尝试从 unsafeWindow 恢复 if (!this._vue && typeof unsafeWindow !== 'undefined' && unsafeWindow && unsafeWindow.Vue) { this._vue = unsafeWindow.Vue; // 同时恢复到 window,确保 TDesign 组件能访问 if (typeof window !== 'undefined') { window.Vue = this._vue; } console.log('[shuake] 🔄 Vue recovered from unsafeWindow'); } return this._vue || null; }, get TDesign() { // 实时检测 TDesign if (!this._tdesign && typeof window !== 'undefined' && window.TDesign) { this._tdesign = window.TDesign; console.log('[shuake] ✅ TDesign loaded dynamically'); } // 二次防御:从 unsafeWindow 恢复 if (!this._tdesign && typeof unsafeWindow !== 'undefined' && unsafeWindow && unsafeWindow.TDesign) { this._tdesign = unsafeWindow.TDesign; if (typeof window !== 'undefined') { window.TDesign = this._tdesign; } console.log('[shuake] 🔄 TDesign recovered from unsafeWindow'); } return this._tdesign || null; }, // 库是否就绪的便捷检查 get hasJQuery() { return !!this.$; }, get hasAjaxHook() { return !!this.ah; }, get hasVMDom() { return !!this.VM; }, get hasVue() { return !!this.Vue; }, get hasTDesign() { return !!this.TDesign; }, get hasVuePanel() { // Vue 面板可用:Vue 必须就绪,且 TDesign 全局 API 暴露(tdesign-vue-next 全局构建会挂 window.TDesign) return !!(this.Vue && this.TDesign); } }; // ========== TDesign 样式加载器 ========== // 设计目标: // 1. 仅注入面板所需的 TDesign CSS(按需) // 2. CDN 失败时静默降级 // 3. 所有 TDesign 类名前缀隔离为 t-,通过 #shuake-panel 子树限定作用域 const TDesignLoader = { CSS_URL: 'https://cdn.jsdelivr.net/npm/tdesign-vue-next@1.10.6/dist/tdesign.css', _cssInjected: false, ensureCSS() { if (this._cssInjected) return true; if (document.querySelector('link[data-shuake-td-css]')) { this._cssInjected = true; return true; } const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = this.CSS_URL; link.dataset.shuakeTdCss = '1'; link.crossOrigin = 'anonymous'; link.onload = () => { this._cssInjected = true; }; link.onerror = () => { try { console.warn('[shuake] TDesign CSS 加载失败,将降级使用原生样式'); } catch (_) {} }; (document.head || document.documentElement).appendChild(link); this._cssInjected = true; return true; }, get isCSSReady() { return this._cssInjected; } }; // 安全的样式注入:优先 GM_addStyle,缺失时退回 DOM