// ==UserScript== // @name 超星小助手 // @namespace https://ayu.local/cx-h5p // @version 5.14.2 // @description 超星/学习通学习辅助 | 自动播放视频、任务点自动切换、AI 搜题答题、本地题库、视频工具(倍速/截图/下载/画中画/全屏)。永久免费·无使用限制。 // @license MIT // @author 阿宇 // @match *://*.chaoxing.com/* // @match *://chaoxing.com/* // @match *://*.xuexitong.com/* // @match *://xuexitong.com/* // @connect bing.com // @connect baidu.com // @connect www.bing.com // @connect www.baidu.com // @connect api.deepseek.com // @grant unsafeWindow // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getTab // @grant GM_saveTab // @grant GM_getTabs // @grant GM_openInTab // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @run-at document-start // ==/UserScript== /* * 版权与来源说明 * - 本脚本全部代码均为自有实现,未包含第三方脚本代码。 * - 视频工具(倍速/截图/下载/画中画/全屏)、题库匹配、章节测验答题、任务点识别、搜题、面板等逻辑均为自有实现。 * - 以 MIT 许可证发布。免费使用,允许修改与再分发,需保留本版权说明。 * - 免责声明:本脚本仅供个人学习交流使用,使用风险自负,请遵守所在平台的用户协议。 */ (function () { // ========== 视频工具(自研:截图 / 下载 / 画中画 / 全屏) ========== // 倍速逻辑在下方「倍速与强制守护」。本模块不依赖任何第三方脚本。 // 选出当前最值得操作的视频:优先正在播放、其次画面面积最大 function pickActiveVideo() { const list = []; for (const v of getAllVideos()) { let r; try { r = v.getBoundingClientRect(); } catch (e) { continue; } if (!r || r.width < 40 || r.height < 40) continue; const src = v.currentSrc || v.src || ''; if (!src && !v.videoWidth) continue; list.push({ v, area: r.width * r.height, playing: !v.paused && !v.ended }); } if (!list.length) return null; list.sort((a, b) => (b.playing - a.playing) || (b.area - a.area)); return list[0].v; } function videoFileName(v, ext) { let name = ''; try { const src = v.currentSrc || v.src || ''; if (src && !/^(blob|data):/i.test(src)) { const u = new URL(src, location.href); name = decodeURIComponent((u.pathname.split('/').pop() || '')).replace(/\.[^.]+$/, ''); } } catch (e) {} if (!name) { name = String(document.title || 'video').replace(/[-_|].*$/, '').trim().slice(0, 40) || 'video'; } name = name.replace(/[\\/:*?"<>|\s]+/g, '_'); return `${name}_${Date.now()}.${ext}`; } function saveBlob(blob, filename) { let url = ''; try { url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); return true; } catch (e) { return false; } finally { if (url) setTimeout(() => { try { URL.revokeObjectURL(url); } catch (e) {} }, 5000); } } // 截图:canvas 取当前帧存 PNG function shotVideo(v) { v = v || pickActiveVideo(); if (!v) { setStatus('截图:未找到可截取的视频', 'warn'); return; } const w = v.videoWidth, h = v.videoHeight; if (!w || !h) { setStatus('截图:画面还没加载完成', 'warn'); return; } try { const c = document.createElement('canvas'); c.width = w; c.height = h; c.getContext('2d').drawImage(v, 0, 0, w, h); const finish = (blob) => { if (!blob) { setStatus('截图:生成图片失败', 'err'); return; } const ok = saveBlob(blob, videoFileName(v, 'png')); setStatus(ok ? '截图已保存' : '截图保存失败', ok ? 'ok' : 'err'); }; if (typeof c.toBlob === 'function') { c.toBlob(finish, 'image/png'); } else { const b64 = c.toDataURL('image/png').split(',')[1]; const bin = atob(b64); const buf = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i); finish(new Blob([buf], { type: 'image/png' })); } } catch (e) { setStatus('截图失败:该视频源不允许截取画面', 'err'); } } // 下载:直链视频直接取 Blob 保存;m3u8 分片流提示不支持 function downloadVideo(v) { v = v || pickActiveVideo(); if (!v) { setStatus('下载:未找到视频', 'warn'); return; } const src = v.currentSrc || v.src || ''; if (!src) { setStatus('下载:没拿到视频地址', 'warn'); return; } if (/\.m3u8(\?|#|$)/i.test(src) || /mpegurl/i.test(v.type || '')) { setStatus('下载:该视频是 m3u8 分片流,暂不支持直接下载', 'warn'); return; } const name = videoFileName(v, 'mp4'); const done = (blob) => { if (!blob || !blob.size) { setStatus('下载:获取视频失败(可能是防盗链)', 'err'); return; } setStatus(saveBlob(blob, name) ? `下载中:${(blob.size / 1048576).toFixed(1)}MB 已保存` : '下载保存失败', 'ok'); }; const fail = () => setStatus('下载:获取视频失败(可能是防盗链)', 'err'); try { if (typeof GM_xmlhttpRequest === 'function' && /^https?:/i.test(src)) { setStatus('下载:正在获取视频…', 'warn'); GM_xmlhttpRequest({ method: 'GET', url: src, responseType: 'blob', timeout: 120000, onprogress: (e) => { if (e && e.lengthComputable && e.total) { setStatus(`下载:${Math.round(e.loaded / e.total * 100)}%`, 'warn'); } }, onload: (r) => { if (r.status >= 200 && r.status < 400 && r.response) done(r.response); else fail(); }, onerror: fail, ontimeout: fail }); return; } setStatus('下载:正在获取视频…', 'warn'); fetch(src, { credentials: 'include' }) .then(r => r.ok ? r.blob() : Promise.reject(new Error('http ' + r.status))) .then(done) .catch(fail); } catch (e) { fail(); } } // 画中画:进入 / 退出 function togglePiP(v) { v = v || pickActiveVideo(); if (!v) { setStatus('画中画:未找到视频', 'warn'); return; } if (!document.pictureInPictureEnabled || typeof v.requestPictureInPicture !== 'function') { setStatus('画中画:当前浏览器不支持', 'err'); return; } if (document.pictureInPictureElement) { document.exitPictureInPicture() .then(() => setStatus('已退出画中画', 'ok')) .catch(() => setStatus('退出画中画失败', 'err')); return; } v.requestPictureInPicture() .then(() => setStatus('已进入画中画', 'ok')) .catch((e) => setStatus('画中画不可用:' + ((e && e.message) || '需要先点一下视频'), 'err')); } // 全屏:优先全屏播放器容器,其次 video 本身 function toggleFullscreen(v) { v = v || pickActiveVideo(); if (!v) { setStatus('全屏:未找到视频', 'warn'); return; } let el = v; try { el = v.closest('[class*="video"],[class*="player"],.vjs-player,.xgplayer,.prism-player') || v.parentElement || v; } catch (e) {} try { if (document.fullscreenElement || document.webkitFullscreenElement) { (document.exitFullscreen || document.webkitExitFullscreen || function () {}).call(document); setStatus('已退出全屏', 'ok'); return; } const req = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen; if (!req) { setStatus('全屏:当前浏览器不支持', 'err'); return; } const r = req.call(el); if (r && r.then) r.then(() => setStatus('已进入全屏', 'ok')).catch(() => setStatus('全屏失败(可能需要先点一下页面)', 'err')); else setStatus('已进入全屏', 'ok'); } catch (e) { setStatus('全屏失败:' + ((e && e.message) || '浏览器限制'), 'err'); } } // 快捷键:Alt+S / Alt+D / Alt+P / Alt+F try { document.addEventListener('keydown', (e) => { if (!e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; const t = e.target; if (t && (/^(input|textarea|select)$/i.test(t.tagName) || t.isContentEditable)) return; const key = String(e.key || '').toLowerCase(); const fn = { s: shotVideo, d: downloadVideo, p: togglePiP, f: toggleFullscreen }[key]; if (!fn) return; e.preventDefault(); e.stopPropagation(); fn(); }, true); } catch (e) {} // ========== 倍速与强制守护(自研,不依赖第三方脚本) ========== let currentRate = 1; let forceAutoRate = false; // 开机第一时间抓取"原生" playbackRate 存取器(防止被页面脚本改写后拿不到) let NATIVE_RATE_GET = null, NATIVE_RATE_SET = null; try { const nd = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); if (nd && nd.get && nd.set) { NATIVE_RATE_GET = nd.get; NATIVE_RATE_SET = nd.set; } } catch (e) {} // 递归获取所有 video(含同源 iframe) function getAllVideos() { const vids = []; function collect(doc, depth) { if (depth > 6) return; try { doc.querySelectorAll('video').forEach(v => vids.push(v)); doc.querySelectorAll('iframe').forEach(f => { try { if (f.contentDocument) collect(f.contentDocument, depth + 1); } catch (e) {} }); } catch (e) {} } collect(document, 0); return vids; } // 强制设置单个 video 的倍速(原生 setter + 实例锁定,平台改不回去) function forceVideoRate(v, rate) { try { // 1. 用原生 setter 直接改(绕过页面自定义的 setter) if (NATIVE_RATE_SET) { try { NATIVE_RATE_SET.call(v, rate); } catch (e) {} } else { v.playbackRate = rate; } try { if (v.defaultPlaybackRate !== rate) v.defaultPlaybackRate = rate; } catch (e) {} // 2. 在实例上锁定 playbackRate:平台再赋值也保持目标倍速 if (v.__cxLockRate !== rate) { v.__cxLockRate = rate; try { Object.defineProperty(v, 'playbackRate', { configurable: true, enumerable: true, get() { try { return NATIVE_RATE_GET ? NATIVE_RATE_GET.call(v) : rate; } catch (e) { return rate; } }, set() { /* 忽略平台的修改,倍速保持锁定 */ } }); } catch (e) {} } // 3. 最后兜底:万一仍被改回去,再设一次 if (Math.abs(v.playbackRate - rate) > 0.01 && NATIVE_RATE_SET) { try { NATIVE_RATE_SET.call(v, rate); } catch (e) {} } } catch (e) {} } // 解除锁定(停止挂机时调用,恢复正常可控) function unlockVideos() { for (const v of getAllVideos()) { try { delete v.playbackRate; } catch (e) {} try { delete v.__cxLockRate; } catch (e) {} } } // 只处理当前窗口的 video(跨域 iframe 里由各自脚本实例处理) function applyRateLocal(rate) { try { document.querySelectorAll('video').forEach(v => forceVideoRate(v, rate)); } catch (e) {} } // 向所有子 frame 广播倍速(跨域 iframe 也能收到) function broadcastRate(rate) { const send = (win) => { try { win.postMessage({ __cxRate: rate }, '*'); } catch (e) {} let frames, len = 0; try { frames = win.frames; len = frames.length; } catch (e) { return; } for (let i = 0; i < len; i++) { try { send(frames[i]); } catch (e) {} } }; try { send(window); } catch (e) {} } // 每个窗口(含跨域 iframe)都监听倍速广播并本地强制 let frameRate = 0; // 由上级窗口广播过来的目标倍速(仅 iframe 使用) try { window.addEventListener('message', (e) => { if (e.source === window) return; // 忽略自己发出的广播 const d = e.data; if (!d || typeof d !== 'object' || typeof d.__cxRate !== 'number') return; if (d.__cxRate < 0) { frameRate = 0; return; } // 停止挂机,解除强制 frameRate = d.__cxRate; applyRateLocal(d.__cxRate); }); } catch (e) {} // 平台若在本窗口把倍速改回去,立即再强制一次(ratechange 实时纠偏) try { document.addEventListener('ratechange', (e) => { const v = e.target; if (!v || v.tagName !== 'VIDEO') return; const target = forceAutoRate ? currentRate : frameRate; if (!target) return; if (Math.abs(v.playbackRate - target) > 0.01) { setTimeout(() => forceVideoRate(v, target), 50); } }, true); } catch (e) {} function setRate(rate) { currentRate = rate; // 本窗口 + 同源 iframe 的 video applyRateLocal(rate); for (const v of getAllVideos()) forceVideoRate(v, rate); // 通知所有 iframe(含跨域)各自强制 broadcastRate(rate); } // 独立的高频倍速守护(不依赖 tick;电脑端播放器会频繁把倍速改回 1x) let rateTimer = null; function startRateKeeper() { if (rateTimer) return; rateTimer = setInterval(() => { if (!forceAutoRate) return; applyRateLocal(currentRate); for (const v of getAllVideos()) forceVideoRate(v, currentRate); broadcastRate(currentRate); }, 500); } function stopRateKeeper() { if (rateTimer) { clearInterval(rateTimer); rateTimer = null; } } // 定时确保倍速不被重置(顶层 + 跨域 iframe 广播) function ensureRate() { if (!forceAutoRate) return; setRate(currentRate); } function initPanel() { // 只在顶层窗口创建面板,避免 iframe 重复创建 if (window.top !== window.self) return; if (window.__cxPanel) return; window.__cxPanel = true; const host = document.createElement('div'); host.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2147483647;'; document.documentElement.appendChild(host); const shadow = host.attachShadow({ mode: 'open' }); window.__shadow = shadow; // 暴露给题库弹窗等功能使用 const style = document.createElement('style'); style.textContent = ` .box{width:200px;font:13px/1.5 system-ui,"Microsoft YaHei",sans-serif; color:#e8eaed;background:#1f2430;border:1px solid #39415a; border-radius:10px;box-shadow:0 6px 24px rgba(0,0,0,.35);overflow:hidden} .hd{display:flex;align-items:center;justify-content:space-between; padding:10px 12px;background:#2a3247;cursor:move;user-select:none; -webkit-user-select:none;touch-action:none} .hd b{font-size:13px;font-weight:600} .hd .fold{cursor:pointer;opacity:.7;padding:0 6px;font-size:16px} .bd{padding:10px 12px} .row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px} .btn{flex:1;padding:9px 0;border:0;border-radius:6px;cursor:pointer; font-size:13px;font-weight:600;color:#fff;background:#3d6bf5} .btn.off{background:#59627a} select{padding:6px 8px;border-radius:6px;border:1px solid #39415a; background:#1a1e28;color:#e8eaed;font-size:13px;cursor:pointer} .tip{color:#9aa3b8;font-size:12px} .ok{color:#4ade80} .warn{color:#fbbf24} .tools{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px} .tbtn{padding:7px 0;border:0;border-radius:6px;cursor:pointer; font-size:12px;color:#e8eaed;background:#2a3247;border:1px solid #39415a} .tbtn:active{background:#3d6bf5} .linkbtn{display:block;text-align:center;padding:8px 0;border-radius:6px; font-size:13px;color:#93c5fd;background:#1a1e28;border:1px solid #39415a; text-decoration:none} .linkbtn:active{background:#3d6bf5;color:#fff} .mbtn{flex:1;padding:6px 0;border:1px solid #39415a;border-radius:6px; font-size:12px;color:#9aa3b8;background:#1a1e28;cursor:pointer} .mbtn.on{color:#fff;background:#3d6bf5;border-color:#3d6bf5} .gbtn{padding:4px 10px;border:0;border-radius:6px;font-size:12px; color:#fff;background:#3d6bf5;cursor:pointer;white-space:nowrap} .gbtn:active{background:#2d5be5} .sec{margin-bottom:8px;border:1px solid #39415a;border-radius:8px;background:#1a1e28;overflow:hidden} .sec summary{display:flex;align-items:center;padding:7px 10px;cursor:pointer; font-size:12px;color:#cbd5e1;list-style:none;user-select:none;-webkit-user-select:none} .sec summary::-webkit-details-marker{display:none} .sec summary>span:first-child{flex:1} .sec summary::after{content:'▾';color:#7c869e;font-size:10px;margin-left:8px} .sec[open] summary::after{content:'▴'} .secbody{padding:8px 10px 0;border-top:1px solid #2a3247} .badge{font-size:11px;color:#93c5fd;background:#232a3a;border-radius:10px;padding:1px 8px} .orow{display:flex;align-items:center;margin-bottom:8px} .orow input{margin-right:6px} #dsRow{display:flex;gap:6px;align-items:center} .banklink{color:#93c5fd;text-decoration:none;cursor:pointer} .banklink:active{color:#bfdbfe} .bankmask{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:2147483646; display:flex;align-items:center;justify-content:center;padding:20px} .bankbox{background:#1e2330;border-radius:12px;padding:16px;width:100%;max-width:420px; max-height:80vh;display:flex;flex-direction:column} .bankhd{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px} .bankhd b{color:#fff;font-size:15px} .bankclose{color:#9aa3b8;font-size:20px;cursor:pointer;background:none;border:0;padding:0 6px} .banktools{display:flex;gap:6px;margin-left:auto;margin-right:8px} .banktools button{padding:3px 8px;background:none;border:1px solid #3b4a6b;color:#93c5fd; border-radius:5px;font-size:12px;cursor:pointer} .banklist{overflow-y:auto;flex:1} .bankitem{padding:10px 0;border-bottom:1px solid #2a3247;font-size:13px;color:#e8eaed} .bankitem .bq{color:#cbd5e1;margin-bottom:4px;word-break:break-all} .bankitem .ba{color:#4ade80;font-weight:bold} .bankempty{color:#9aa3b8;text-align:center;padding:30px 0} .bankclear{margin-top:10px;padding:8px;border:1px solid #ef4444;border-radius:6px; background:none;color:#ef4444;font-size:12px;cursor:pointer} .bankclear:active{background:#ef4444;color:#fff} .ctxm{position:fixed;left:0;top:0;z-index:2147483647;display:none;min-width:158px; background:#1f2430;border:1px solid #39415a;border-radius:8px;padding:4px; box-shadow:0 8px 28px rgba(0,0,0,.5);color:#e8eaed;font-size:13px} .ctxm.show{display:block} .ctxm .ti{font-size:11px;opacity:.5;padding:2px 10px 4px} .ctxm .it{padding:8px 10px;border-radius:6px;cursor:pointer;white-space:nowrap} .ctxm .it:hover{background:#2b3346} .ctxm .sep{height:1px;background:#39415a;margin:4px 2px} `; shadow.appendChild(style); const box = document.createElement('div'); box.className = 'box'; box.innerHTML = `
超星小助手v5.14.2—
倍速
任务点:识别中…
状态:待启动
进度:-
答题设置跳过答题
题库0 题
其他功能工具
快捷键:Alt+S / Alt+D / Alt+P / Alt+F
✓ 永久免费 · 无使用限制
仅供学习交流 · 使用风险自负
`; shadow.appendChild(box); const $fold = box.querySelector('.fold'); const $body = box.querySelector('.bd'); const $hang = box.querySelector('[data-act=hang]'); const $rate = box.querySelector('.rate'); const $mute = box.querySelector('.mute'); const $skip = box.querySelector('.skip'); const $stat = box.querySelector('.stat'); const $tasktype = box.querySelector('.tasktype'); const $prog = box.querySelector('.prog'); const $banksize = box.querySelector('.banksize'); const $banklink = box.querySelector('.banklink'); const $ai = box.querySelector('.ai'); const $aiModeRow = box.querySelector('#aiModeRow'); const $dsRow = box.querySelector('#dsRow'); const $dsTip = box.querySelector('#dsTip'); const $dskey = box.querySelector('.dskey'); const $getkeyBtn = box.querySelector('[data-act=getkey]'); const $modeBtns = box.querySelectorAll('.mbtn'); // 初始化题库题数显示 try { $banksize.textContent = getBankSize(); } catch (e) {} // 倍速选项(新存储键,默认 1x,忽略旧版本保存的值) let savedRate = 1; try { savedRate = parseInt(GM_getValue('cx_rate2', '1')) || 1; } catch (e) {} for (let r = 1; r <= 16; r++) { const o = document.createElement('option'); o.value = String(r); o.textContent = r + 'x'; if (r === savedRate) o.selected = true; $rate.appendChild(o); } currentRate = savedRate; $fold.addEventListener('click', () => { const hidden = $body.style.display === 'none'; $body.style.display = hidden ? '' : 'none'; $fold.textContent = hidden ? '—' : '+'; }); $rate.addEventListener('change', () => { currentRate = parseFloat($rate.value) || 1; try { GM_setValue('cx_rate2', String(currentRate)); } catch (e) {} forceAutoRate = true; // 选了倍速就持续保持 setRate(currentRate); // 选倍速后立即生效,不管是否挂机 startRateKeeper(); // 开启高频守护,电脑端也不被改回 1x }); $hang.addEventListener('click', () => { if (running) stopHang(); else startHang(); }); // 加载已保存的搜题模式(key 或 search) let aiMode = 'key'; try { aiMode = GM_getValue('ai_mode', 'key') || 'key'; } catch (e) {} // 切换模式按钮高亮 function setAiMode(mode) { aiMode = mode; try { GM_setValue('ai_mode', mode); } catch (e) {} $modeBtns.forEach(b => { b.classList.toggle('on', b.dataset.mode === mode); }); // 选 Key 才显示输入框和提示 const showDs = mode === 'key'; $dsRow.style.display = showDs ? '' : 'none'; $dsTip.style.display = showDs ? '' : 'none'; } // AI 开关:打开时显示模式选择行;两个答题模式互斥,开 AI 就关掉"跳过答题" $ai.addEventListener('change', () => { if ($ai.checked) { $skip.checked = false; try { GM_setValue('cx_quiz_mode', 'ai'); } catch (e) {} } else { try { GM_setValue('cx_quiz_mode', ''); } catch (e) {} } $aiModeRow.style.display = $ai.checked ? '' : 'none'; if ($ai.checked) setAiMode(aiMode); else { $dsRow.style.display = 'none'; $dsTip.style.display = 'none'; } try { savePrefs(); } catch (e) {} try { updateQmodeBadge(); } catch (e) {} }); // "跳过答题":勾上就取消 AI搜题答题(互斥) $skip.addEventListener('change', () => { if ($skip.checked) { $ai.checked = false; $aiModeRow.style.display = 'none'; $dsRow.style.display = 'none'; $dsTip.style.display = 'none'; try { GM_setValue('cx_quiz_mode', 'skip'); } catch (e) {} } else { try { GM_setValue('cx_quiz_mode', ''); } catch (e) {} } try { savePrefs(); } catch (e) {} try { updateQmodeBadge(); } catch (e) {} }); // 模式按钮点击 $modeBtns.forEach(b => { b.addEventListener('click', () => setAiMode(b.dataset.mode)); }); // 初始化模式显示 setAiMode(aiMode); // ========== 复选框状态持久化(刷新不丢) ========== const PREFS_KEY = 'cx_prefs'; const $checkboxes = box.querySelectorAll('input[type="checkbox"]'); // 加载已保存的勾选状态 try { const prefs = JSON.parse(GM_getValue(PREFS_KEY, '{}') || '{}'); $checkboxes.forEach(cb => { const cls = cb.className; if (prefs.hasOwnProperty(cls)) cb.checked = prefs[cls]; }); } catch (e) {} // 勾选变化时自动保存 function savePrefs() { try { const prefs = {}; $checkboxes.forEach(cb => { prefs[cb.className] = cb.checked; }); GM_setValue(PREFS_KEY, JSON.stringify(prefs)); } catch (e) {} } $checkboxes.forEach(cb => cb.addEventListener('change', savePrefs)); // 折叠区徽标:随时显示当前答题模式 const $qmodeBadge = box.querySelector('.qmode'); function updateQmodeBadge() { if (!$qmodeBadge) return; $qmodeBadge.textContent = $ai.checked ? 'AI搜题' : ($skip.checked ? '跳过答题' : '未开启'); } updateQmodeBadge(); // 旧版本可能两个答题模式都勾着:只保留一个(按上次选择的模式,没记录则优先 AI搜题答题) if ($skip.checked && $ai.checked) { let savedMode = ''; try { savedMode = GM_getValue('cx_quiz_mode', '') || ''; } catch (e) {} if (savedMode === 'skip') $ai.checked = false; else $skip.checked = false; savePrefs(); } // AI 开关恢复时同步显示模式行 $aiModeRow.style.display = $ai.checked ? '' : 'none'; if ($ai.checked) setAiMode(aiMode); else { $dsRow.style.display = 'none'; $dsTip.style.display = 'none'; } updateQmodeBadge(); // 加载已保存的 DeepSeek Key(刷新不丢) try { const savedKey = GM_getValue('ds_api_key', ''); if (savedKey) $dskey.value = savedKey; } catch (e) {} // 多重事件兜底保存:input / change / blur / paste,确保手机端粘贴也能记住 function saveDsKey() { try { GM_setValue('ds_api_key', $dskey.value.trim()); } catch (e) {} } $dskey.addEventListener('input', saveDsKey); $dskey.addEventListener('change', saveDsKey); $dskey.addEventListener('blur', saveDsKey); $dskey.addEventListener('paste', () => setTimeout(saveDsKey, 100)); // 获取Key按钮:一键直接跳转到 API Keys 创建页 // 用动态 标签点击,浏览器原生行为,不会被拦截 $getkeyBtn.addEventListener('click', () => { const url = 'https://platform.deepseek.com/api_keys'; try { const a = document.createElement('a'); a.href = url; a.target = '_blank'; a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } catch (e) { location.href = url; } }); // 点击题库链接:显示全部已保存题目 $banklink.addEventListener('click', () => showBankDetail()); // ========== 保存设置 / 测试 AI / 题库导入导出 ========== const $bankfile = box.querySelector('.bankfile'); const $savecfgBtn = box.querySelector('[data-act=savecfg]'); const $testaiBtn = box.querySelector('[data-act=testai]'); const $importbankBtn = box.querySelector('[data-act=importbank]'); const $exportbankBtn = box.querySelector('[data-act=exportbank]'); // 保存设置:把当前所有勾选、倍速、搜题模式、Key 一次性落盘 $savecfgBtn.addEventListener('click', () => { try { savePrefs(); saveDsKey(); GM_setValue('cx_rate2', String(currentRate)); GM_setValue('ai_mode', aiMode); GM_setValue('cx_quiz_mode', $ai.checked ? 'ai' : ($skip.checked ? 'skip' : '')); } catch (e) {} setStatus('设置已保存(刷新不会丢)', 'ok'); }); // 测试 AI:Key 模式测 DeepSeek,搜索引擎模式测 Bing 连通性 $testaiBtn.addEventListener('click', async () => { if (aiMode === 'key') { const key = ($dskey.value || '').trim(); if (!key) { setStatus('请先填写 DeepSeek Key', 'err'); return; } setStatus('测试 DeepSeek 中…'); try { GM_xmlhttpRequest({ method: 'POST', url: 'https://api.deepseek.com/v1/chat/completions', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + key }, data: JSON.stringify({ model: 'deepseek-chat', messages: [{ role: 'user', content: '回复:OK' }], temperature: 0, max_tokens: 5 }), timeout: 20000, onload: (resp) => { try { const data = JSON.parse(resp.responseText || '{}'); const c = data.choices && data.choices[0] && data.choices[0].message ? data.choices[0].message.content : ''; if (resp.status === 200 && c) setStatus('AI 可用:' + String(c).trim().slice(0, 20), 'ok'); else if (resp.status === 401) setStatus('AI 测试失败:Key 无效(401)', 'err'); else if (resp.status === 402 || resp.status === 403) setStatus('AI 测试失败:余额不足或无权限', 'err'); else setStatus('AI 测试失败:HTTP ' + resp.status, 'err'); } catch (e) { setStatus('AI 测试失败:返回无法解析', 'err'); } }, onerror: () => setStatus('AI 测试失败:网络错误', 'err'), ontimeout: () => setStatus('AI 测试失败:请求超时', 'err') }); } catch (e) { setStatus('AI 测试失败:请求发不出去', 'err'); } } else { setStatus('测试搜索引擎中…'); try { const txt = await fetchSearchText('https://www.bing.com/search?q=' + encodeURIComponent('学习通')); if (txt && txt.length > 300) setStatus('搜索引擎可用(Bing)', 'ok'); else setStatus('Bing 请求失败或被拦截,可换 Key 模式', 'err'); } catch (e) { setStatus('搜索引擎测试失败', 'err'); } } }); // 导入题库 $importbankBtn.addEventListener('click', () => $bankfile.click()); $bankfile.addEventListener('change', (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const n = importBankJson(String(reader.result || '')); try { $banksize.textContent = getBankSize(); } catch (e2) {} if (n > 0) setStatus(`题库导入 ${n} 题,共 ${getBankSize()} 题`, 'ok'); else setStatus('题库导入失败:不是合法 JSON', 'err'); }; reader.readAsText(file, 'utf-8'); e.target.value = ''; }); // 导出题库 $exportbankBtn.addEventListener('click', () => { if (getBankSize() === 0) { setStatus('题库为空,无可导出内容'); return; } exportBankJson(); }); // ========== 视频工具按钮(截图 / 下载 / 画中画 / 全屏) ========== const $shotBtn = box.querySelector('[data-act=shot]'); const $dlBtn = box.querySelector('[data-act=dl]'); const $pipBtn = box.querySelector('[data-act=pip]'); const $fsBtn = box.querySelector('[data-act=fs]'); if ($shotBtn) $shotBtn.addEventListener('click', () => shotVideo()); if ($dlBtn) $dlBtn.addEventListener('click', () => downloadVideo()); if ($pipBtn) $pipBtn.addEventListener('click', () => togglePiP()); if ($fsBtn) $fsBtn.addEventListener('click', () => toggleFullscreen()); // ========== 鼠标右键快捷菜单(在鼠标位置弹出学习通入口) ========== const $ctxmenu = box.querySelector('.ctxmenu'); const $ctx = document.createElement('div'); $ctx.className = 'ctxm'; shadow.appendChild($ctx); let ctxOpen = false; const CX_LINKS = [ { t: '📖 超星首页', u: 'https://www.chaoxing.com/' }, { t: '📚 我的课程', u: 'https://i.chaoxing.com/' }, { t: '🎬 开始学习', u: 'https://mooc1.chaoxing.com/' }, { t: '🔑 获取 DeepSeek Key', u: 'https://platform.deepseek.com/api_keys' } ]; function openUrl(url) { try { const a = document.createElement('a'); a.href = url; a.target = '_blank'; a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } catch (e) { location.href = url; } } function hideCtx() { $ctx.classList.remove('show'); ctxOpen = false; } function showCtx(x, y) { $ctx.innerHTML = '
超星快捷入口
' + CX_LINKS.map((l, i) => `
${l.t}
`).join('') + '
✕ 关闭菜单
'; $ctx.classList.add('show'); const r = $ctx.getBoundingClientRect(); const px = Math.max(8, Math.min(x, window.innerWidth - r.width - 8)); const py = Math.max(8, Math.min(y, window.innerHeight - r.height - 8)); $ctx.style.left = px + 'px'; $ctx.style.top = py + 'px'; ctxOpen = true; } $ctx.addEventListener('click', (e) => { const it = e.target.closest('.it'); if (!it) return; const i = parseInt(it.dataset.i, 10); hideCtx(); if (i >= 0 && CX_LINKS[i]) openUrl(CX_LINKS[i].u); }); document.addEventListener('contextmenu', (e) => { if (!$ctxmenu.checked) return; const t = e.target; const tag = (t && t.tagName || '').toLowerCase(); // 输入框、可编辑区、已选中文字时不劫持,保留原生菜单 if (tag === 'input' || tag === 'textarea' || (t && t.isContentEditable)) return; if (window.getSelection && String(window.getSelection()).length > 0) return; e.preventDefault(); showCtx(e.clientX, e.clientY); }, true); document.addEventListener('mousedown', (e) => { if (!ctxOpen) return; const path = e.composedPath ? e.composedPath() : []; if (path.includes($ctx)) return; hideCtx(); }, true); window.addEventListener('scroll', hideCtx, true); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideCtx(); }); // 拖动 let sx=0, sy=0, ox=0, oy=0, on=false; host.style.touchAction = 'none'; host.addEventListener('pointerdown', (e) => { const path = e.composedPath ? e.composedPath() : []; for (const el of path) { if (el === host) break; if (el.nodeType !== 1) continue; if (el.matches && el.matches('button,select,input,label,summary,a')) return; } on = true; sx = e.clientX; sy = e.clientY; const r = host.getBoundingClientRect(); ox = r.left; oy = r.top; host.style.right = 'auto'; host.style.left = ox + 'px'; }); document.addEventListener('pointermove', (e) => { if (!on) return; let nx = ox + (e.clientX - sx); let ny = oy + (e.clientY - sy); nx = Math.max(0, Math.min(window.innerWidth - host.offsetWidth, nx)); ny = Math.max(0, Math.min(window.innerHeight - host.offsetHeight, ny)); host.style.left = nx + 'px'; host.style.top = ny + 'px'; }); document.addEventListener('pointerup', () => { on = false; }); document.addEventListener('pointercancel', () => { on = false; }); // 页面初始渲染完成后应用一次倍速 setTimeout(() => { setRate(currentRate); }, 0); window.__ui = { $stat, $prog, $hang, $banksize, $tasktype }; // 页面跳转(切任务点/换章节)后自动恢复挂机,5 分钟内有效 try { const saved = JSON.parse(GM_getValue('cx_running', '') || '{}'); if (isChaoxing && saved && saved.on && Date.now() - (saved.t || 0) < 5 * 60 * 1000) { setTimeout(() => { if (!running) startHang(); }, 1200); } } catch (e) {} } function setStatus(text, cls) { if (!window.__ui) return; window.__ui.$stat.textContent = text; window.__ui.$stat.className = 'stat' + (cls ? ' ' + cls : ''); } function setProg(text) { if (!window.__ui) return; window.__ui.$prog.textContent = text; } function setTaskType(text) { if (!window.__ui || !window.__ui.$tasktype) return; if (window.__ui.$tasktype.textContent === text) return; window.__ui.$tasktype.textContent = text; } // ========== 学习通自动刷课逻辑 ========== const isChaoxing = /chaoxing\.com|xuexitong\.com|xueyinonline\.com|mooc[1-9]?\.chaoxing\.com/i.test(location.href); let running = false; let tickTimer = null; let quizCooldown = 0; let lastNextClickTime = 0; // 上次点"下一节"的时间戳 const NEXT_CLICK_INTERVAL = 8000; // 两次点击之间最小间隔(毫秒),AI 答题不受此限制 let scrollCount = 0; let fallbackCooldown = 0; // 兜底跳转(滚动/目录)的冷却,避免每秒重复执行 function startHang() { if (running) return; running = true; forceAutoRate = true; try { GM_setValue('cx_running', JSON.stringify({ on: true, t: Date.now() })); } catch (e) {} if (window.__ui) { window.__ui.$hang.textContent = '■ 停止挂机'; window.__ui.$hang.classList.remove('off'); } setRate(currentRate); startRateKeeper(); setStatus('启动中…'); docScrollState = null; fontDecryptAttempted = false; if (tickTimer) clearInterval(tickTimer); tickTimer = setInterval(tick, 1000); // 后台保活:静音音频循环 + 切回标签页立即恢复播放 initBackgroundKeepAlive(); if (isChaoxing) tick(); } // ========== 后台保活 ========== let keepAliveAudio = null; function initBackgroundKeepAlive() { if (keepAliveAudio) return; try { // 用一个静音音频循环播放,防止浏览器把标签页过度节流 keepAliveAudio = document.createElement('audio'); keepAliveAudio.loop = true; keepAliveAudio.muted = true; keepAliveAudio.volume = 0; // base64 编码的极短静音 wav keepAliveAudio.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; keepAliveAudio.play().catch(() => {}); } catch (e) {} // 切回标签页时立即恢复播放 document.addEventListener('visibilitychange', () => { if (!document.hidden) { const videos = collectVideos(); videos.forEach(item => { try { if (item.v.paused) item.v.play().catch(() => {}); } catch (e) {} }); } }); // 每 2 秒尝试恢复后台暂停的视频(即使标签页不可见) setInterval(() => { if (document.querySelector('.keepalive')?.checked === false) return; const videos = collectVideos(); videos.forEach(item => { try { if (item.v.paused && !item.v.ended) { item.v.muted = true; item.v.play().catch(() => {}); } } catch (e) {} }); }, 2000); } function stopHang() { running = false; forceAutoRate = false; try { GM_setValue('cx_running', ''); } catch (e) {} if (tickTimer) { clearInterval(tickTimer); tickTimer = null; } stopRateKeeper(); // 通知所有 iframe 解除倍速强制 broadcastRate(-1); unlockVideos(); if (window.__ui) { window.__ui.$hang.textContent = '▶ 开始挂机'; window.__ui.$hang.classList.add('off'); } setStatus('已停止'); } function collectVideos() { const vids = []; function collect(doc, depth) { if (depth > 5) return; try { doc.querySelectorAll('video').forEach(v => { // 跳过隐藏/未激活的视频(避免处理其它任务点的残留播放器) try { const r = v.getBoundingClientRect(); if (r.width < 10 || r.height < 10) return; } catch (e) {} vids.push({ v, doc }); }); doc.querySelectorAll('iframe').forEach(f => { try { if (f.contentDocument) collect(f.contentDocument, depth + 1); } catch (e) {} }); } catch (e) {} } collect(document, 0); return vids; } // 递归收集所有 document(包括 iframe),供跨框架检测使用 function getAllDocs() { const docs = []; function collect(d, depth) { if (depth > 5 || !d) return; if (docs.indexOf(d) === -1) docs.push(d); try { d.querySelectorAll('iframe').forEach(f => { try { if (f.contentDocument) collect(f.contentDocument, depth + 1); } catch (e) {} }); } catch (e) {} } try { collect(document, 0); } catch (e) {} return docs; } // 找到当前视频所属的任务点容器(.ans-attach-ct / .ans-job-ct) function getTaskContainerFor(v, doc) { try { for (const d of getAllDocs()) { let iframes; try { iframes = d.querySelectorAll('iframe'); } catch (e) { continue; } for (const f of iframes) { let cd = null; try { cd = f.contentDocument || (f.contentWindow && f.contentWindow.document); } catch (e) {} if (cd && cd === doc) { const box = f.closest('.ans-attach-ct, .ans-job-ct, .ans-attach'); if (box) return box; } } } } catch (e) {} try { const box = v && v.closest && v.closest('.ans-attach-ct, .ans-job-ct, .ans-attach'); if (box) return box; } catch (e) {} return null; } function isTaskCompleted(doc, v) { try { // 1) 只看"当前这个视频"的任务点容器,避免章节里其它任务已完成导致误判 const box = getTaskContainerFor(v, doc); if (box) { // 容器自身或上两级祖先带完成标记 let n = box; for (let i = 0; i < 3 && n; i++, n = n.parentElement) { const cls = (n.className || '').toString(); if (/ans-job-finished/.test(cls)) return true; if (/已完成/.test((n.getAttribute && n.getAttribute('aria-label')) || '')) return true; } try { const icon = box.querySelector('.ans-job-icon[aria-label*="已完成"], .ans-job-icon[title*="已完成"]'); if (icon) return true; } catch (e) {} const t = (box.textContent || '').replace(/\s+/g, ''); if (/任务点已完成/.test(t)) return true; return false; // 容器明确存在且未完成 } // 2) 定位不到容器时,只在视频所在文档内判断,不做全页搜索 if (doc && doc.body) { const t = (doc.body.innerText || '').replace(/\s+/g, ''); if (/任务点已完成/.test(t)) return true; } } catch (e) {} return false; } function isQuizPage() { // 跨 iframe 判断:有题目特征(题型/单选圈)才算答题页 const docs = getAllDocs(); for (const d of docs) { if (!d.body) continue; // 学习通标准测验结构(最可靠) try { if (d.querySelector('input[id^="answertype"]') || d.querySelector('.TiMu, .singleQuesId, .Zy_TItle, #form1 #RightCon')) return true; } catch (e) {} const text = d.body.innerText || ''; if (/(单选题|多选题|判断题)/.test(text) && /(测验|答题|题目|下列)/.test(text)) return true; if (/测验/.test(text) && d.querySelector('input[type="radio"], input[type="checkbox"], .singleQuesId, .mulQuesId, .judgeQuesId')) { return true; } } return false; } // 判断任务点标签/目录项是否处于"当前选中"状态(只认完整类名词,避免 icon/button 被误判) function isActiveTaskEl(el, ignoreCursor) { try { let n = el; for (let i = 0; i < 4 && n && n.getAttribute; i++, n = n.parentElement) { const cls = (n.className || '').toString(); if (/posCatalog_select/i.test(cls)) return true; if (/(^|\s)_select(\s|$)/.test(cls)) return true; const tokens = cls.split(/\s+/); for (const t of tokens) { if (/^(active|current|selected|cur|curr|checked|focus|on)$/i.test(t)) return true; } const style = n.getAttribute('style') || ''; if (!ignoreCursor && /cursor:\s*default/i.test(style)) return true; if (n.getAttribute('aria-selected') === 'true') return true; } } catch (e) {} return false; } // 是否位于右侧目录树(章节列表)里 —— 目录里的"07 直播/08 阅读"是章节名,不是任务点 function inCatalogTree(el) { try { let n = el; for (let i = 0; i < 6 && n && n.getAttribute; i++, n = n.parentElement) { const cls = (n.className || '').toString(); const id = (n.id || '').toString(); if (/posCatalog_level|chapterCatalog|catalogTree|catalogBox/i.test(cls)) return true; if (/catalog/i.test(id)) return true; } const r = el.getBoundingClientRect ? el.getBoundingClientRect() : null; // 右侧目录区(视口右侧 38%) if (r && r.width > 0 && r.left > (window.innerWidth || 1000) * 0.62) return true; } catch (e) {} return false; } function isLiveStream() { // 检测直播任务:只认内容区的直播任务点,绝不把右侧目录里的"07 直播"章节名当成直播 try { const docs = getAllDocs(); for (const d of docs) { // 1. 直播播放器 / 直播专属按钮 → 当前确实在直播任务里 try { if (d.querySelector('iframe[src*="live"], iframe[src*="zhibo"], [id*="livePlayer"], [class*="livePlayer"], [class*="live-player"], [class*="live_room"], .liveBox')) return true; } catch (e) {} let btns = []; try { btns = d.querySelectorAll('button, a, span, div'); } catch (e) {} for (const b of btns) { const t = (b.textContent || '').trim(); if (t !== '进入直播' && t !== '观看直播' && t !== '进入直播间') continue; const r = b.getBoundingClientRect(); if (r.width > 0 && r.height > 0 && !inCatalogTree(b)) return true; } // 2. 当前激活的任务点标签是"直播"(单个序号,排除右侧目录树) let labels = []; try { labels = d.querySelectorAll('.ans-attach-ct, .ans-job-ct, .ans-attach, .posCatalog_select'); } catch (e) {} for (const el of labels) { let t = ''; try { t = (el.textContent || '').replace(/\s+/g, ''); } catch (e) {} if (!t || t.length > 8) continue; if (!/^\d?直播(视频|回放|任务)?$/.test(t)) continue; if (inCatalogTree(el)) continue; const r = el.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) continue; if (isActiveTaskEl(el, true)) return true; } } } catch (e) {} return false; } // ========== 视频弹题自动作答 ========== let quizState = { answered: false, secondTry: false, aiTried: false }; function isVideoQuiz() { // 检测视频弹题:选项+提交 或 题型标题+提交 const docs = getAllDocs(); let hasVideo = false; for (const d of docs) { try { if (d.body && /