// ==UserScript== // @name 学习通刷课助手 // @namespace http://tampermonkey.net/ // @version 1.2.1 // @description 学习通视频自动播放+自动答题一站式助手 // @author 叶屿 // @license GPL3 // @antifeature payment 题库答题功能需要验证码(免费)或激活码(付费),视频播放等基础功能完全免费 // @match *://*.chaoxing.com/* // @run-at document-end // @icon https://www.chaoxing.com/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_getResourceText // @grant unsafeWindow // @connect *.chaoxing.com // @connect qsy.iano.cn // @connect lyck6.cn // @connect open.bigmodel.cn // @connect chizhu-dl.github.io // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js // @require https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js // @require https://cdn.jsdelivr.net/combine/gh/photopea/Typr.js@15aa12ffa6cf39e8788562ea4af65b42317375fb/src/Typr.min.js,gh/photopea/Typr.js@f4fcdeb8014edc75ab7296bd85ac9cde8cb30489/src/Typr.U.min.js // @resource fontTable https://chizhu-dl.github.io/font-decrypt/Table.json // ==/UserScript== /* * ========================================== * 学习通刷课助手 v1.2.1 * ========================================== * * 【功能说明】 * 1. 视频自动播放(支持倍速、自动推进章节、自动跳过测验) * 2. 作业自动答题(题库查询 + AI 答题) * 3. 考试自动答题(支持停止/继续) * 4. 双模式题库(免费验证码 / 付费激活码) * * 【积分购买】 * 联系微信:C919irt * 价格表:50积分=2元,100积分=4元,150积分=6元,200积分=8元,500积分=18元 * 说明:每次答题消耗1积分,积分永久有效 * * 【付费声明】 * 本脚本基础功能(视频播放)完全免费 * 题库答题功能需要验证码(免费24小时)或激活码(付费永久) * 付费仅用于题库API调用成本,不强制购买 * * 【免责声明】 * 本脚本仅供学习交流使用,请勿用于违反学校规定或作弊行为 * 使用本脚本造成的任何后果由使用者自行承担 * * 【版权信息】 * 作者:叶屿 | 版本:v1.2.1 * * ========================================== */ (() => { 'use strict'; // 防止在子 iframe 中重复执行(学习通页面有大量嵌套 iframe) if (window.top !== window.self) return; // ========================================== // ConfigStore - 配置存储模块 // ========================================== const ConfigStore = { // AES 加密内置密钥(补齐到 16 字节) _key: CryptoJS.enc.Utf8.parse('chaoxing_cfg_key'), _iv: CryptoJS.enc.Utf8.parse('chaoxing_cfg_key'), // 通用存取(GM_setValue / GM_getValue) get(key) { const raw = GM_getValue(key, undefined); if (raw === undefined) return undefined; try { return JSON.parse(raw); } catch (_) { return raw; } }, set(key, value) { GM_setValue(key, JSON.stringify(value)); }, remove(key) { GM_deleteValue(key); }, clearAll() { this.remove('cx_video_config'); this.remove('cx_answer_config'); localStorage.removeItem('cx_device_id'); localStorage.removeItem('cx_run_state'); }, // 密码加密 / 解密(AES/CBC/PKCS7) encryptPassword(plaintext) { return CryptoJS.AES.encrypt(plaintext, this._key, { iv: this._iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString(); }, decryptPassword(ciphertext) { const bytes = CryptoJS.AES.decrypt(ciphertext, this._key, { iv: this._iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return bytes.toString(CryptoJS.enc.Utf8); }, // 设备 ID(首次生成 UUID,持久化到 localStorage) getDeviceId() { let id = localStorage.getItem('cx_device_id'); if (!id) { id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); localStorage.setItem('cx_device_id', id); } return id; }, // 视频播放配置 getVideoConfig() { return this.get('cx_video_config') || { playbackRate: 2, autoAdvance: true, autoSkipQuiz: true, autoAnswerQuiz: true, quizMode: 'free' }; }, setVideoConfig(config) { this.set('cx_video_config', config); }, // 答题模块配置 getAnswerConfig() { return this.get('cx_answer_config') || { mode: 'free', verifyValidUntil: 0, activationCode: '', aiApiKey: '' }; }, setAnswerConfig(config) { this.set('cx_answer_config', config); }, // 运行状态(localStorage) getRunState() { const raw = localStorage.getItem('cx_run_state'); if (!raw) return null; try { return JSON.parse(raw); } catch (_) { return null; } }, setRunState(state) { localStorage.setItem('cx_run_state', JSON.stringify(state)); }, clearRunState() { localStorage.removeItem('cx_run_state'); } }; // ========================================== // Logger - 日志模块 // ========================================== const Logger = { _entries: [], _onLog: null, log(message, level = 'info') { const entry = { timestamp: new Date().toISOString(), level, message }; this._entries.push(entry); if (this._entries.length > 50) { this._entries.shift(); } if (typeof this._onLog === 'function') { this._onLog(entry); } }, clearLogs() { this._entries = []; }, getEntries() { return this._entries; } }; // ========================================== // UIPanel - UI 面板模块 // ========================================== const UIPanel = { _iframe: null, _doc: null, _isMinimized: false, _activeTab: 'video', _settingsVisible: false, _running: false, _startTime: null, _timerInterval: null, // 2.1 - init(): 创建 iframe 并注入面板 HTML/CSS init() { if (this._iframe) return; const iframe = document.createElement('iframe'); iframe.id = 'cx-helper-iframe'; iframe.setAttribute('frameborder', '0'); iframe.setAttribute('allowtransparency', 'true'); Object.assign(iframe.style, { position: 'fixed', top: '20px', right: '20px', left: 'auto', width: '480px', height: '520px', zIndex: '999999', border: 'none', borderRadius: '14px', background: '#fff', overflow: 'hidden', boxShadow: '0 20px 60px rgba(0,0,0,0.12), 0 8px 20px rgba(0,0,0,0.06)' }); document.body.appendChild(iframe); this._iframe = iframe; const doc = iframe.contentDocument || iframe.contentWindow.document; this._doc = doc; doc.open(); doc.write(this._buildHTML()); doc.close(); // 绑定事件 this._bindEvents(); // 启用标题栏拖拽 this.enableDrag(doc.getElementById('cx-header')); // 连接 Logger Logger._onLog = (entry) => this.log(entry.message, entry.level); // 渲染已有日志 Logger.getEntries().forEach(e => this.log(e.message, e.level)); }, // 2.1 - destroy(): 移除 iframe destroy() { if (this._iframe) { this._iframe.remove(); this._iframe = null; this._doc = null; } if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } Logger._onLog = null; }, // 2.2 - enableDrag(): 绑定标题栏拖拽 enableDrag(handleEl) { if (!handleEl || !this._iframe) return; const iframe = this._iframe; const doc = this._doc; let isDragging = false; let startX = 0, startY = 0, startLeft = 0, startTop = 0; const onMove = (e) => { if (!isDragging) return; const dx = e.screenX - startX; const dy = e.screenY - startY; const maxLeft = Math.max(0, window.innerWidth - iframe.offsetWidth); const maxTop = Math.max(0, window.innerHeight - iframe.offsetHeight); iframe.style.left = Math.min(Math.max(0, startLeft + dx), maxLeft) + 'px'; iframe.style.top = Math.min(Math.max(0, startTop + dy), maxTop) + 'px'; iframe.style.right = 'auto'; }; const stopDrag = () => { if (!isDragging) return; isDragging = false; iframe.style.transition = ''; doc.body.style.userSelect = ''; }; handleEl.addEventListener('mousedown', (e) => { isDragging = true; startX = e.screenX; startY = e.screenY; // Convert right-positioned to left-positioned for dragging const rect = iframe.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; iframe.style.left = startLeft + 'px'; iframe.style.right = 'auto'; iframe.style.transition = 'none'; doc.body.style.userSelect = 'none'; e.preventDefault(); }); doc.addEventListener('mousemove', onMove); window.addEventListener('mousemove', onMove); doc.addEventListener('mouseup', stopDrag); window.addEventListener('mouseup', stopDrag); window.addEventListener('blur', stopDrag); }, // 2.3 - minimize(): 收缩为 50×50 悬浮图标 minimize() { if (this._isMinimized || !this._iframe) return; this._isMinimized = true; const doc = this._doc; doc.getElementById('cx-panel').style.display = 'none'; doc.getElementById('cx-mini').classList.add('show'); this._iframe.style.width = '50px'; this._iframe.style.height = '50px'; this._iframe.style.borderRadius = '50%'; }, // 2.3 - restore(): 恢复完整面板 restore() { if (!this._isMinimized || !this._iframe) return; this._isMinimized = false; const doc = this._doc; doc.getElementById('cx-panel').style.display = ''; doc.getElementById('cx-mini').classList.remove('show'); this._iframe.style.width = '480px'; this._iframe.style.height = '520px'; this._iframe.style.borderRadius = '14px'; }, // 2.4 - switchTab(): 切换标签页 switchTab(tab) { if (!this._doc) return; this._activeTab = tab; const doc = this._doc; // 更新标签按钮样式 doc.querySelectorAll('.cx-tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.tab === tab); }); // 更新内容区 doc.querySelectorAll('.cx-tab-content').forEach(el => { el.style.display = el.dataset.tab === tab ? 'block' : 'none'; }); }, // 2.5 - showSettings(): 显示设置面板 showSettings() { if (!this._doc) return; this._settingsVisible = !this._settingsVisible; this._doc.getElementById('cx-settings').style.display = this._settingsVisible ? 'block' : 'none'; }, // 2.6 - log(): 在日志区域追加日志 log(message, level = 'info') { if (!this._doc) return; const logArea = this._doc.getElementById('cx-log-area'); if (!logArea) return; const div = this._doc.createElement('div'); div.className = 'cx-log-entry cx-log-' + level; const time = new Date().toLocaleTimeString('zh-CN', { hour12: false }); const icon = level === 'success' ? '✓' : level === 'error' ? '✗' : '·'; div.textContent = `${icon} ${time} ${message}`; logArea.appendChild(div); // 限制最多 50 条 while (logArea.children.length > 50) { logArea.removeChild(logArea.firstChild); } logArea.scrollTop = logArea.scrollHeight; }, // 2.6 - clearLogs(): 清空日志区域 clearLogs() { if (!this._doc) return; const logArea = this._doc.getElementById('cx-log-area'); if (logArea) logArea.innerHTML = ''; }, // 2.6 - setStatus(): 设置状态栏文字 setStatus(text, type = 'info') { if (!this._doc) return; const el = this._doc.getElementById('cx-status-text'); if (!el) return; el.textContent = text; el.className = 'cx-status-text cx-status-' + type; }, // 2.6 - setRunning(): 设置运行状态 setRunning(isRunning) { this._running = isRunning; if (!this._doc) return; const indicator = this._doc.getElementById('cx-status-indicator'); const timerEl = this._doc.getElementById('cx-status-timer'); // 先清理旧的定时器,防止内存泄漏 if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } if (isRunning) { this._startTime = Date.now(); if (indicator) indicator.className = 'cx-status-indicator running'; this._timerInterval = setInterval(() => { if (timerEl && this._startTime) { const elapsed = Math.floor((Date.now() - this._startTime) / 1000); const h = String(Math.floor(elapsed / 3600)).padStart(2, '0'); const m = String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0'); const s = String(elapsed % 60).padStart(2, '0'); timerEl.textContent = `${h}:${m}:${s}`; } }, 1000); } else { this._startTime = null; if (indicator) indicator.className = 'cx-status-indicator'; if (timerEl) timerEl.textContent = '00:00:00'; if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } } }, // 2.6 - setProgress(): 设置进度 setProgress(current, total) { if (!this._doc) return; const el = this._doc.getElementById('cx-status-progress'); if (el) el.textContent = total > 0 ? `${current}/${total}` : ''; }, // 2.6 - setCountdown(): 设置倒计时 setCountdown(seconds) { if (!this._doc) return; const el = this._doc.getElementById('cx-status-countdown'); if (!el) return; if (seconds > 0) { const h = String(Math.floor(seconds / 3600)).padStart(2, '0'); const m = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0'); const s = String(seconds % 60).padStart(2, '0'); el.textContent = `倒计时 ${h}:${m}:${s}`; el.style.display = 'inline'; } else { el.textContent = ''; el.style.display = 'none'; } }, // 2.6 - setButtonEnabled(): 设置按钮启用/禁用 setButtonEnabled(buttonId, enabled) { if (!this._doc) return; const btn = this._doc.getElementById(buttonId); if (btn) btn.disabled = !enabled; }, // 2.6 - setButtonLoading(): 设置按钮加载状态 setButtonLoading(buttonId, loading) { if (!this._doc) return; const btn = this._doc.getElementById(buttonId); if (!btn) return; if (loading) { btn._origText = btn._origText || btn.textContent; btn.textContent = '处理中...'; btn.disabled = true; } else { btn.textContent = btn._origText || btn.textContent; btn.disabled = false; } }, // 内部:构建面板 HTML _buildHTML() { return `
📚
📖 学习通刷课助手
⚙️
⚠️ 有BUG及时反馈需提供账号,联系微信:C919irt 📋
视频助手
自动答题
当前章节: 未开始
播放进度: 0%
⚙️ 播放设置
播放倍速
⚙️ 设置
视频助手
答题模式
数据管理

清除所有存储数据,包括账号、密码、配置和日志。此操作不可撤销。

就绪
00:00:00
`; }, // 内部:构建 CSS _buildCSS() { return ` html, body { margin:0; padding:0; overflow:hidden; height:100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "PingFang SC", Arial, sans-serif; color: #2c3e50; background: transparent; font-size: 13px; } * { box-sizing:border-box; } /* 最小化图标 */ .cx-mini { position:absolute; inset:0; background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; border-radius:50%; display:none; align-items:center; justify-content:center; cursor:pointer; font-size:22px; box-shadow:0 4px 16px rgba(99,102,241,0.45); transition:all .3s; } .cx-mini:hover { transform:scale(1.1); box-shadow:0 6px 22px rgba(99,102,241,0.55); } .cx-mini.show { display:flex; } /* 主面板 */ .cx-panel { width:100%; height:100%; background:#f8fafc; border-radius:16px; position:relative; overflow:hidden; display:flex; flex-direction:column; max-height:100vh; } /* 标题栏 */ .cx-header { height:48px; background:linear-gradient(135deg,#6366f1 0%,#8b5cf6 50%,#a78bfa 100%); color:#fff; display:flex; align-items:center; justify-content:space-between; padding:0 18px; cursor:move; flex-shrink:0; user-select:none; } .cx-header-title { font-size:14px; font-weight:700; letter-spacing:0.3px; text-shadow:0 1px 3px rgba(0,0,0,0.12); } .cx-header-tools { display:flex; gap:12px; } .cx-tool-btn { cursor:pointer; font-size:15px; opacity:.85; transition:all .2s; padding:4px 6px; border-radius:6px; } .cx-tool-btn:hover { opacity:1; transform:scale(1.12); background:rgba(255,255,255,0.18); } /* 标签栏 */ .cx-tab-bar { display:flex; background:#fff; border-bottom:1px solid #e2e8f0; flex-shrink:0; padding:0 10px; } .cx-tab-btn { flex:1; text-align:center; padding:10px 0; font-size:12.5px; cursor:pointer; color:#94a3b8; transition:all .25s; border-bottom:2px solid transparent; font-weight:500; margin:0 2px; border-radius:6px 6px 0 0; } .cx-tab-btn:hover { color:#6366f1; background:rgba(99,102,241,0.04); } .cx-tab-btn.active { color:#6366f1; border-bottom-color:#6366f1; font-weight:700; background:#fff; } /* 反馈提示条 */ .cx-feedback-tip { background:#fffbeb; border-bottom:1px solid #fde68a; padding:6px 14px; font-size:11px; color:#92400e; line-height:1.5; flex-shrink:0; } .cx-feedback-wechat { display:inline-block; background:#fff; padding:1px 8px; border-radius:4px; border:1px solid #e2e8f0; cursor:pointer; transition:all .2s; font-weight:600; font-size:11px; margin-left:2px; } .cx-feedback-wechat:hover { border-color:#6366f1; color:#6366f1; background:#eef2ff; } /* 内容区 — 关键:允许滚动 */ .cx-body { flex:1 1 0; overflow-y:auto; overflow-x:hidden; padding:14px; min-height:0; background:#f8fafc; } .cx-body::-webkit-scrollbar { width:5px; } .cx-body::-webkit-scrollbar-track { background:transparent; } .cx-body::-webkit-scrollbar-thumb { background:#cbd5e1; border-radius:10px; } .cx-body::-webkit-scrollbar-thumb:hover { background:#94a3b8; } /* 设置面板 */ .cx-settings { display:none; position:absolute; top:48px; left:0; width:100%; height:calc(100% - 48px); background:#f8fafc; z-index:99; overflow-y:auto; box-sizing:border-box; } .cx-settings::-webkit-scrollbar { width:5px; } .cx-settings::-webkit-scrollbar-thumb { background:#cbd5e1; border-radius:3px; } .cx-settings-header { display:flex; justify-content:space-between; align-items:center; padding:14px 18px; background:#fff; border-bottom:1px solid #e2e8f0; font-weight:700; font-size:14px; color:#1e293b; } .cx-settings-close { cursor:pointer; font-size:16px; color:#94a3b8; transition:all .2s; width:28px; height:28px; display:flex; align-items:center; justify-content:center; border-radius:8px; } .cx-settings-close:hover { color:#ef4444; background:#fef2f2; } .cx-settings-body { padding:14px; } .cx-settings-section { background:#fff; border-radius:12px; padding:16px; margin-bottom:12px; box-shadow:0 1px 3px rgba(0,0,0,0.04); border:1px solid #e2e8f0; transition:box-shadow .2s; } .cx-settings-section:hover { box-shadow:0 2px 8px rgba(0,0,0,0.06); } .cx-settings-title { font-weight:700; font-size:13px; margin-bottom:10px; color:#1e293b; } .cx-settings-danger-zone { border:1px solid #fecaca; background:#fef2f2; } .cx-form-item { margin-bottom:10px; } .cx-form-item label { font-size:13px; color:#64748b; } .cx-form-item select { padding:7px 12px; border:1.5px solid #e2e8f0; border-radius:8px; font-size:13px; background:#fff; margin-left:8px; outline:none; transition:border-color .2s; } .cx-form-item select:focus { border-color:#6366f1; } .cx-checkbox-label { display:flex; align-items:center; gap:8px; cursor:pointer; padding:6px 8px; border-radius:8px; transition:background .2s; } .cx-checkbox-label:hover { background:#eef2ff; } .cx-checkbox-label input { margin:0; width:16px; height:16px; cursor:pointer; accent-color:#6366f1; } .cx-radio-group { display:flex; flex-direction:column; gap:4px; } .cx-radio-option { display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:8px; cursor:pointer; transition:background .2s; font-size:13px; } .cx-radio-option:hover { background:#eef2ff; } .cx-radio-option input { margin:0; cursor:pointer; accent-color:#6366f1; } .cx-settings-footer { display:flex; gap:10px; justify-content:center; margin-top:14px; } /* 按钮 */ .cx-btn { padding:9px 20px; border:none; border-radius:10px; font-size:12.5px; font-weight:600; cursor:pointer; transition:all .2s; letter-spacing:0.2px; } .cx-btn:hover { transform:translateY(-1px); box-shadow:0 4px 12px rgba(0,0,0,0.08); } .cx-btn:active { transform:translateY(0); } .cx-btn-primary { background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; } .cx-btn-primary:hover { box-shadow:0 4px 16px rgba(99,102,241,0.35); } .cx-btn-secondary { background:#f1f5f9; color:#64748b; } .cx-btn-secondary:hover { background:#e2e8f0; box-shadow:0 2px 8px rgba(0,0,0,0.04); } .cx-btn-success { background:linear-gradient(135deg,#10b981,#059669); color:#fff; } .cx-btn-success:hover { box-shadow:0 4px 16px rgba(16,185,129,0.35); } .cx-btn-warning { background:linear-gradient(135deg,#f59e0b,#d97706); color:#fff; } .cx-btn-warning:hover { box-shadow:0 4px 16px rgba(245,158,11,0.35); } .cx-btn-danger { background:linear-gradient(135deg,#ef4444,#dc2626); color:#fff; width:100%; } .cx-btn-danger:hover { box-shadow:0 4px 16px rgba(239,68,68,0.35); } .cx-btn-info { background:#f1f5f9; color:#64748b; } .cx-btn-info:hover { background:#e2e8f0; } .cx-btn:disabled { opacity:.45; cursor:not-allowed; transform:none; box-shadow:none; } /* 日志区域 */ .cx-log-wrap { flex-shrink:0; position:relative; border-top:1px solid #e2e8f0; background:#f8fafc; } .cx-log-area { height:100px; overflow-y:auto; padding:8px 14px; box-sizing:border-box; font-size:11px; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif; mask-image:linear-gradient(to bottom, transparent 0%, black 20%, black 100%); -webkit-mask-image:linear-gradient(to bottom, transparent 0%, black 20%, black 100%); } .cx-log-area::-webkit-scrollbar { width:3px; } .cx-log-area::-webkit-scrollbar-track { background:transparent; } .cx-log-area::-webkit-scrollbar-thumb { background:#cbd5e1; border-radius:3px; } .cx-log-entry { padding:3px 8px; margin:2px 0; line-height:1.5; border-radius:6px; font-size:11px; animation:cx-log-in .3s ease; } @keyframes cx-log-in { from{opacity:0;transform:translateY(4px);} to{opacity:1;transform:translateY(0);} } .cx-log-info { color:#64748b; background:#f1f5f9; } .cx-log-success { color:#059669; background:#ecfdf5; } .cx-log-error { color:#dc2626; background:#fef2f2; } /* 状态栏 */ .cx-status-bar { height:32px; background:#fff; border-top:1px solid #e2e8f0; display:flex; align-items:center; justify-content:space-between; padding:0 14px; font-size:11px; color:#94a3b8; flex-shrink:0; } .cx-status-left { display:flex; align-items:center; gap:8px; } .cx-status-right { display:flex; align-items:center; } .cx-status-indicator { width:7px; height:7px; border-radius:50%; background:#cbd5e1; display:inline-block; } .cx-status-indicator.running { background:#10b981; animation:cx-pulse 1.5s infinite; } @keyframes cx-pulse { 0%,100%{opacity:1;} 50%{opacity:.4;} } .cx-status-text { color:#64748b; } .cx-status-text.cx-status-info { color:#64748b; } .cx-status-text.cx-status-success { color:#10b981; } .cx-status-text.cx-status-error { color:#ef4444; } .cx-status-progress { color:#6366f1; font-weight:700; } .cx-status-countdown { color:#f59e0b; font-weight:700; } .cx-status-timer { font-family:'JetBrains Mono',Consolas,"Courier New",monospace; color:#94a3b8; font-size:11px; } /* 表单通用 */ .cx-form-row { display:flex; flex-direction:column; gap:4px; } .cx-form-row-inline { flex-direction:row; gap:10px; } .cx-form-col { flex:1; display:flex; flex-direction:column; gap:4px; } .cx-form-label { font-size:12px; color:#64748b; font-weight:600; } .cx-required { color:#ef4444; margin-left:2px; } .cx-input { padding:9px 12px; border:1.5px solid #e2e8f0; border-radius:10px; font-size:13px; outline:none; transition:all .2s; box-sizing:border-box; width:100%; background:#fff; } .cx-input:focus { border-color:#6366f1; box-shadow:0 0 0 3px rgba(99,102,241,0.1); } .cx-input.cx-input-error { border-color:#ef4444; box-shadow:0 0 0 3px rgba(239,68,68,0.1); } .cx-form-error { font-size:11px; color:#ef4444; min-height:14px; } /* 加载动画 */ .cx-loading::after { content:''; display:inline-block; width:12px; height:12px; border:2px solid rgba(255,255,255,0.4); border-top-color:#fff; border-radius:50%; animation:cx-spin .6s linear infinite; margin-left:6px; vertical-align:middle; } @keyframes cx-spin { to{transform:rotate(360deg);} } /* 视频面板 */ .cx-video-panel { display:flex; flex-direction:column; gap:12px; } .cx-video-status-card { background:#fff; border-radius:14px; padding:16px; box-shadow:0 1px 3px rgba(0,0,0,0.04); border:1px solid #e2e8f0; transition:box-shadow .2s; } .cx-video-status-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.06); } .cx-video-info-row { display:flex; align-items:center; margin-bottom:8px; font-size:13px; } .cx-video-label { color:#94a3b8; min-width:80px; flex-shrink:0; font-weight:500; } .cx-video-value { color:#1e293b; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .cx-video-progress-wrap { margin-top:6px; } .cx-video-progress-track { height:6px; background:#e2e8f0; border-radius:3px; overflow:hidden; } .cx-video-progress-bar { height:100%; background:linear-gradient(90deg,#6366f1,#10b981); border-radius:3px; transition:width .5s ease; min-width:0; } .cx-video-actions { display:flex; gap:10px; } .cx-video-actions .cx-btn { flex:1; padding:11px 0; font-size:13px; } /* 答题模块 */ .cx-answer-panel { display:flex; flex-direction:column; gap:10px; } .cx-answer-mode-bar { display:flex; gap:4px; background:#fff; border-radius:12px; padding:4px; border:1px solid #e2e8f0; } .cx-answer-mode-option { flex:1; display:flex; align-items:center; justify-content:center; gap:4px; padding:8px 4px; border-radius:10px; cursor:pointer; font-size:12px; font-weight:600; transition:all .25s; text-align:center; color:#94a3b8; } .cx-answer-mode-option:hover { background:#eef2ff; color:#6366f1; } .cx-answer-mode-option input { display:none; } .cx-answer-mode-option:has(input:checked) { background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; box-shadow:0 2px 8px rgba(99,102,241,0.3); } .cx-answer-area { } .cx-answer-card { background:#fff; border-radius:14px; padding:16px; box-shadow:0 1px 3px rgba(0,0,0,0.04); border:1px solid #e2e8f0; transition:box-shadow .2s; } .cx-answer-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.06); } .cx-answer-card-title { font-weight:700; font-size:13px; color:#1e293b; margin-bottom:12px; } .cx-free-qr-placeholder { display:flex; flex-direction:column; align-items:center; gap:8px; padding:20px; background:#f8fafc; border-radius:12px; border:1.5px dashed #c7d2fe; } .cx-free-qr-icon { font-size:36px; } .cx-free-qr-text { font-size:12px; color:#94a3b8; } .cx-input-group { display:flex; gap:8px; align-items:center; } .cx-btn-sm { padding:8px 16px; font-size:12px; } .cx-btn-xs { padding:5px 10px; font-size:11px; border-radius:6px; } .cx-answer-status { font-size:12px; margin-top:10px; min-height:16px; } .cx-paid-credits-row { display:flex; align-items:center; gap:10px; padding:12px 14px; background:#f8fafc; border-radius:10px; border:1px solid #e2e8f0; } .cx-paid-credits-label { font-size:13px; color:#64748b; } .cx-paid-credits-value { font-size:20px; font-weight:800; color:#6366f1; } .cx-answer-actions { display:flex; gap:8px; margin-top:4px; } .cx-answer-actions .cx-btn { flex:1; } /* 购买信息 */ .cx-buy-info { background:#fffbeb; padding:12px; border-radius:10px; border:1px solid #fde68a; margin-top:12px; font-size:12px; line-height:1.6; } .cx-buy-info-title { font-weight:700; color:#d97706; margin-bottom:8px; font-size:13px; } .cx-buy-info-wechat { margin-bottom:4px; color:#5a6577; } .cx-wechat-copy { display:inline-flex; align-items:center; gap:4px; background:#fff; padding:3px 10px; border-radius:6px; border:1px solid #dce1e8; cursor:pointer; transition:all .2s; font-weight:600; font-size:12px; } .cx-wechat-copy:hover { border-color:#667eea; background:#f0f2ff; color:#667eea; } .cx-price-table { margin-top:6px; font-size:11px; color:#6b7280; line-height:1.8; } /* AI 答题提示 */ .cx-ai-hint { font-size:12px; color:#64748b; margin-bottom:12px; line-height:1.6; } .cx-ai-link { color:#6366f1; text-decoration:none; font-weight:500; } .cx-ai-link:hover { text-decoration:underline; } .cx-ai-free-tip { margin-top:8px; font-size:11px; color:#94a3b8; } /* 视频内联设置卡片 */ .cx-video-settings-card { background:#fff; border-radius:14px; padding:14px 16px; margin-top:12px; box-shadow:0 1px 3px rgba(0,0,0,0.04); border:1px solid #e2e8f0; transition:box-shadow .2s; } .cx-video-settings-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.06); } .cx-card-title { font-weight:700; font-size:13px; color:#1e293b; margin-bottom:10px; } .cx-inline-setting { display:flex; align-items:center; padding:6px 0; } .cx-setting-label { font-size:13px; color:#64748b; margin-right:10px; font-weight:500; } .cx-select-inline { padding:6px 10px; border:1.5px solid #e2e8f0; border-radius:10px; font-size:13px; background:#fff; outline:none; transition:border-color .2s; cursor:pointer; } .cx-select-inline:focus { border-color:#6366f1; } .cx-toggle-label { display:flex; align-items:center; gap:8px; cursor:pointer; font-size:13px; color:#64748b; padding:2px 0; } .cx-toggle-label input { margin:0; width:16px; height:16px; cursor:pointer; accent-color:#6366f1; } /* 底部留白防止滚动不到底 */ .cx-tab-content { padding-bottom:8px; } `; }, // 内部:绑定事件 _bindEvents() { const doc = this._doc; // 最小化按钮 doc.getElementById('cx-btn-minimize').addEventListener('click', () => this.minimize()); // 最小化图标点击恢复 doc.getElementById('cx-mini').addEventListener('click', () => this.restore()); // 反馈微信号复制 doc.getElementById('cx-feedback-wechat')?.addEventListener('click', () => { const el = doc.getElementById('cx-feedback-wechat'); const text = 'C919irt'; if (navigator.clipboard) { navigator.clipboard.writeText(text).then(() => { el.textContent = '已复制 ✅'; setTimeout(() => { el.textContent = 'C919irt 📋'; }, 1500); }).catch(() => {}); } }); // 设置按钮 doc.getElementById('cx-btn-settings').addEventListener('click', () => this.showSettings()); // 设置面板关闭 doc.getElementById('cx-settings-close').addEventListener('click', () => { this._settingsVisible = false; doc.getElementById('cx-settings').style.display = 'none'; }); // 设置面板保存 doc.getElementById('cx-settings-save').addEventListener('click', () => { // 保存视频配置 const playbackRate = parseFloat(doc.getElementById('cx-cfg-playback-rate').value) || 2; const autoAdvance = doc.getElementById('cx-cfg-auto-advance').checked; const autoSkipQuiz = doc.getElementById('cx-cfg-auto-skip-quiz').checked; const autoAnswerQuiz = doc.getElementById('cx-cfg-auto-answer-quiz')?.checked !== false; const quizMode = doc.getElementById('cx-cfg-quiz-mode')?.value || 'free'; ConfigStore.setVideoConfig({ playbackRate, autoAdvance, autoSkipQuiz, autoAnswerQuiz, quizMode }); // 立即更新当前视频速度 if (typeof VideoModule !== 'undefined' && VideoModule.isPlaying()) { VideoModule.setPlaybackRate(playbackRate); } // 同步到内联设置控件 const rateInline = doc.getElementById('cx-video-rate-inline'); if (rateInline) rateInline.value = String(playbackRate); const advInline = doc.getElementById('cx-video-advance-inline'); if (advInline) advInline.checked = autoAdvance; const skipInline = doc.getElementById('cx-video-skip-quiz-inline'); if (skipInline) skipInline.checked = autoSkipQuiz; const autoAnswerInline = doc.getElementById('cx-video-auto-answer-quiz-inline'); if (autoAnswerInline) autoAnswerInline.checked = autoAnswerQuiz; const quizModeInline = doc.getElementById('cx-video-quiz-mode-inline'); if (quizModeInline) { quizModeInline.value = quizMode; quizModeInline.disabled = !autoAnswerQuiz; } // 保存答题模式配置 const answerMode = doc.querySelector('input[name="cx-answer-mode"]:checked')?.value || 'free'; const answerCfg = ConfigStore.getAnswerConfig(); answerCfg.mode = answerMode; ConfigStore.setAnswerConfig(answerCfg); Logger.log('设置已保存', 'success'); this._settingsVisible = false; doc.getElementById('cx-settings').style.display = 'none'; }); // 设置面板取消 doc.getElementById('cx-settings-cancel').addEventListener('click', () => { this._settingsVisible = false; doc.getElementById('cx-settings').style.display = 'none'; }); // 标签页切换 doc.querySelectorAll('.cx-tab-btn').forEach(btn => { btn.addEventListener('click', () => this.switchTab(btn.dataset.tab)); }); // 播放倍速即时更新 doc.getElementById('cx-cfg-playback-rate').addEventListener('change', (e) => { const rate = parseFloat(e.target.value) || 2; if (typeof VideoModule !== 'undefined' && VideoModule.isPlaying()) { VideoModule.setPlaybackRate(rate); } }); // 清除数据按钮 doc.getElementById('cx-btn-clear-data')?.addEventListener('click', () => { if (confirm('确定要清除所有数据吗?此操作不可撤销,将删除所有账号、密码、配置和日志。')) { ConfigStore.clearAll(); Logger.clearLogs(); this.clearLogs(); this.setRunning(false); this.setStatus('数据已清除', 'info'); Logger.log('所有数据已清除', 'success'); } }); // 视频内联设置 — 倍速 doc.getElementById('cx-video-rate-inline')?.addEventListener('change', (e) => { const rate = parseFloat(e.target.value) || 2; const cfg = ConfigStore.getVideoConfig(); cfg.playbackRate = rate; ConfigStore.setVideoConfig(cfg); // 同步设置面板 const settingsRate = doc.getElementById('cx-cfg-playback-rate'); if (settingsRate) settingsRate.value = String(rate); if (typeof VideoModule !== 'undefined' && VideoModule.isPlaying()) { VideoModule.setPlaybackRate(rate); } Logger.log(`播放倍速已设为 ${rate}x`, 'info'); }); // 视频内联设置 — 自动推进 doc.getElementById('cx-video-advance-inline')?.addEventListener('change', (e) => { const cfg = ConfigStore.getVideoConfig(); cfg.autoAdvance = e.target.checked; ConfigStore.setVideoConfig(cfg); const settingsCb = doc.getElementById('cx-cfg-auto-advance'); if (settingsCb) settingsCb.checked = e.target.checked; Logger.log(`自动推进章节: ${e.target.checked ? '开启' : '关闭'}`, 'info'); }); // 视频内联设置 — 跳过弹窗测验 doc.getElementById('cx-video-skip-quiz-inline')?.addEventListener('change', (e) => { const cfg = ConfigStore.getVideoConfig(); cfg.autoSkipQuiz = e.target.checked; ConfigStore.setVideoConfig(cfg); const settingsCb = doc.getElementById('cx-cfg-auto-skip-quiz'); if (settingsCb) settingsCb.checked = e.target.checked; Logger.log(`自动跳过弹窗测验: ${e.target.checked ? '开启' : '关闭'}`, 'info'); }); // 视频内联设置 — 自动答章节测验复选框 doc.getElementById('cx-video-auto-answer-quiz-inline')?.addEventListener('change', (e) => { const cfg = ConfigStore.getVideoConfig(); cfg.autoAnswerQuiz = e.target.checked; ConfigStore.setVideoConfig(cfg); // 联动下拉框禁用状态 const modeSelect = doc.getElementById('cx-video-quiz-mode-inline'); if (modeSelect) modeSelect.disabled = !e.target.checked; // 同步设置面板 const settingsCb = doc.getElementById('cx-cfg-auto-answer-quiz'); if (settingsCb) settingsCb.checked = e.target.checked; const settingsSelect = doc.getElementById('cx-cfg-quiz-mode'); if (settingsSelect) settingsSelect.disabled = !e.target.checked; Logger.log(`自动答章节测验: ${e.target.checked ? '开启' : '关闭'}`, 'info'); }); // 视频内联设置 — 章节测验模式 doc.getElementById('cx-video-quiz-mode-inline')?.addEventListener('change', (e) => { const cfg = ConfigStore.getVideoConfig(); cfg.quizMode = e.target.value; ConfigStore.setVideoConfig(cfg); const settingsSelect = doc.getElementById('cx-cfg-quiz-mode'); if (settingsSelect) settingsSelect.value = e.target.value; const modeNames = { free: '免费题库', paid: '付费题库', ai: 'AI 答题' }; Logger.log(`章节测验模式: ${modeNames[e.target.value] || e.target.value}`, 'info'); }); // 二维码图片加载失败时显示文字占位 const qrImg = doc.getElementById('cx-free-qr-img'); const qrFallback = doc.getElementById('cx-free-qr-fallback'); if (qrImg) { qrImg.onload = () => { qrImg.style.display = 'block'; if (qrFallback) qrFallback.style.display = 'none'; }; qrImg.onerror = () => { qrImg.style.display = 'none'; if (qrFallback) qrFallback.style.display = 'block'; }; } // 微信号复制 const wechatCopy = doc.getElementById('cx-wechat-copy'); if (wechatCopy) { wechatCopy.addEventListener('click', () => { const text = 'C919irt'; if (navigator.clipboard) { navigator.clipboard.writeText(text).then(() => { wechatCopy.textContent = '已复制 ✅'; setTimeout(() => { wechatCopy.textContent = 'C919irt 📋'; }, 1500); }).catch(() => { wechatCopy.textContent = 'C919irt 📋'; }); } }); } } }; // ========================================== // VideoModule - 视频自动播放模块 // ========================================== const VideoModule = { _playing: false, _currentChapter: '', _progress: 0, _videoEl: null, _antiPauseHandlers: [], _pauseCheckInterval: null, _quizCheckInterval: null, _progressInterval: null, _loadRetryCount: 0, _maxLoadRetries: 3, _userPaused: false, _quizAnswering: false, _lastQuizCloseTime: 0, _lastPauseTime: 0, _tryingPlay: false, _pendingPlayTimeout: null, // 6.1 - start(): 开始自动播放 start() { if (this._playing) return; this._playing = true; const cfg = ConfigStore.getVideoConfig(); Logger.log('视频自动播放已启动', 'success'); UIPanel.setRunning(true); UIPanel.setStatus('运行中', 'success'); this._loadRetryCount = 0; this._userPaused = false; this.installAntiPause(); this.playCurrentVideo(); // 启动测验弹窗检测 if (cfg.autoSkipQuiz) { this._quizCheckInterval = setInterval(() => this.detectAndCloseQuiz(), 2000); } // 启动进度更新 this._progressInterval = setInterval(() => this._updateProgress(), 1000); this._updateVideoUI(); }, // 6.1 - stop(): 停止自动播放 stop() { if (!this._playing) return; this._playing = false; this.removeAntiPause(); if (this._quizCheckInterval) { clearInterval(this._quizCheckInterval); this._quizCheckInterval = null; } if (this._progressInterval) { clearInterval(this._progressInterval); this._progressInterval = null; } if (this._pauseCheckInterval) { clearInterval(this._pauseCheckInterval); this._pauseCheckInterval = null; } if (this._pendingPlayTimeout) { clearTimeout(this._pendingPlayTimeout); this._pendingPlayTimeout = null; } this._tryingPlay = false; this._videoEl = null; Logger.log('视频自动播放已停止', 'info'); UIPanel.setRunning(false); UIPanel.setStatus('已停止', 'info'); this._updateVideoUI(); }, // 6.1 - playCurrentVideo(): 检测页面视频元素并播放 async playCurrentVideo() { if (!this._playing) return; this._loadRetryCount = 0; await this._tryPlayVideo(); }, // 内部:尝试查找并播放视频,15秒超时重试 async _tryPlayVideo() { if (!this._playing) return; if (this._tryingPlay) { Logger.log('_tryPlayVideo 已在执行中,跳过重复调用', 'info'); return; } this._tryingPlay = true; try { await this._tryPlayVideoInner(); } finally { this._tryingPlay = false; } }, async _tryPlayVideoInner() { if (!this._playing) return; const video = this._findVideoElement(); if (video) { this._videoEl = video; const cfg = ConfigStore.getVideoConfig(); video.playbackRate = cfg.playbackRate || 2; video.muted = false; try { await video.play(); Logger.log(`视频开始播放,倍速 ${video.playbackRate}x`, 'success'); } catch (e) { // 自动播放可能被浏览器阻止,尝试静音播放 video.muted = true; try { await video.play(); Logger.log('视频已静音播放(浏览器限制)', 'info'); } catch (e2) { Logger.log('视频播放失败: ' + e2.message, 'error'); } } this._updateChapterName(); this._bindVideoEvents(video); this._updateVideoUI(); return; } // 视频未找到,检查是否为章节测验页面(等待iframe加载) await new Promise(r => setTimeout(r, 3000)); const quizDoc = this._findChapterQuizDoc(); if (quizDoc) { const videoCfg = ConfigStore.getVideoConfig(); const autoAnswer = videoCfg.autoAnswerQuiz !== false; const quizMode = videoCfg.quizMode || 'free'; if (autoAnswer) { const modeNames = { free: '免费题库', paid: '付费题库', ai: 'AI 答题' }; Logger.log(`检测到章节测验,模式: ${modeNames[quizMode] || quizMode}`, 'info'); } else { Logger.log('检测到章节测验,未开启自动答题,跳过', 'info'); } await this._autoAnswerChapterQuiz(quizDoc); return; } // 视频未找到,等待加载 if (this._loadRetryCount < this._maxLoadRetries) { Logger.log(`未检测到视频元素,15秒后重试 (${this._loadRetryCount + 1}/${this._maxLoadRetries})`, 'info'); await new Promise(resolve => setTimeout(resolve, 15000)); if (!this._playing) return; this._loadRetryCount++; // 刷新当前章节iframe this._refreshCurrentFrame(); await new Promise(resolve => setTimeout(resolve, 3000)); if (!this._playing) return; await this._tryPlayVideoInner(); } else { Logger.log('视频加载失败,已达最大重试次数', 'error'); UIPanel.setStatus('视频加载失败', 'error'); } }, // 内部:查找视频元素(包括iframe内部,最多4层) _findVideoElement() { // 先在主页面查找 let video = document.querySelector('video'); if (video) return video; // 递归搜索iframe const searchIframes = (doc, depth) => { if (depth > 4) return null; try { const iframes = doc.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (!iframeDoc) continue; const v = iframeDoc.querySelector('video'); if (v) return v; const found = searchIframes(iframeDoc, depth + 1); if (found) return found; } catch (_) { /* 跨域iframe忽略 */ } } } catch (_) {} return null; }; return searchIframes(document, 1); }, // 内部:刷新当前章节iframe _refreshCurrentFrame() { const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { if (iframe.contentDocument?.querySelector('video') !== null || iframe.src) { iframe.src = iframe.src; break; } } catch (_) { /* 跨域忽略 */ } } }, // 内部:在iframe中查找章节测验页面(支持最多4层嵌套iframe) _findChapterQuizDoc() { const quizSelectors = '.questionLi, .singleQuesId, .multipleQuesId, .judgementQuesId, .completionQuesId, .TiMu, .mark_table'; // 先检查主页面 if (document.querySelector(quizSelectors)) return document; // 递归搜索iframe,最多4层深度 const searchIframes = (doc, depth) => { if (depth > 4) return null; try { const iframes = doc.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (!iframeDoc) continue; if (iframeDoc.querySelector(quizSelectors)) return iframeDoc; // 递归搜索更深层 const found = searchIframes(iframeDoc, depth + 1); if (found) return found; } catch (_) { /* 跨域iframe忽略 */ } } } catch (_) {} return null; }; return searchIframes(document, 1); }, // 内部:自动答章节测验 async _autoAnswerChapterQuiz(quizDoc) { // 防止重复调用 if (this._quizAnswering) { Logger.log('章节测验正在进行中,跳过重复调用', 'info'); return; } this._quizAnswering = true; try { await this._doAutoAnswerChapterQuiz(quizDoc); } finally { this._quizAnswering = false; } }, async _doAutoAnswerChapterQuiz(quizDoc) { const videoCfg = ConfigStore.getVideoConfig(); const autoAnswer = videoCfg.autoAnswerQuiz !== false; const quizMode = videoCfg.quizMode || 'free'; // 未勾选自动答题:直接推进下一章 if (!autoAnswer) { Logger.log('章节测验:未开启自动答题,推进下一章节', 'info'); if (this._playing) { setTimeout(() => this.advanceToNextChapter(), 2000); } return; } // 检查对应答题模式是否可用 const answerCfg = ConfigStore.getAnswerConfig(); if (quizMode === 'free') { if (!answerCfg.verifyValidUntil || answerCfg.verifyValidUntil < Date.now() / 1000) { Logger.log('章节测验:免费验证码未验证或已过期,跳过答题', 'info'); if (this._playing) { setTimeout(() => this.advanceToNextChapter(), 2000); } return; } } else if (quizMode === 'ai') { if (!answerCfg.aiApiKey) { Logger.log('章节测验:未配置AI API Key,跳过答题', 'info'); if (this._playing) { setTimeout(() => this.advanceToNextChapter(), 2000); } return; } } // 付费模式不做前置检查,直接尝试答题(积分不足时API会返回错误) // 临时切换答题模式为视频设置中选择的模式(不持久化) const prevMode = AnswerModule._mode; AnswerModule._mode = quizMode; const containers = quizDoc.querySelectorAll( '.questionLi, .singleQuesId, .multipleQuesId, .judgementQuesId, .completionQuesId' ); if (containers.length === 0) { Logger.log('章节测验未找到题目,跳过', 'info'); AnswerModule._mode = prevMode; if (this._playing) { setTimeout(() => this.advanceToNextChapter(), 2000); } return; } Logger.log(`章节测验共 ${containers.length} 题,开始自动答题(模式: ${quizMode})`, 'info'); // 付费模式下先预热API,避免冷启动导致第一题失败 if (quizMode === 'paid') { Logger.log('🔄 正在预热题库API...', 'info'); await new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: 'http://lyck6.cn/scriptService/api/autoFreeAnswer', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ question: '改变政府购买支出水平对宏观经济活动的效果要大于改变税收和转移支付的效果。', options: ['错', '对'], type: 3, location: 'chaoxing.com' }), timeout: 25000, onload: () => { Logger.log('✅ API预热完成', 'info'); resolve(); }, onerror: () => { Logger.log('⚠️ API预热失败,继续答题', 'info'); resolve(); }, ontimeout: () => { Logger.log('⚠️ API预热超时,继续答题', 'info'); resolve(); } }); }); } for (let i = 0; i < containers.length; i++) { if (!this._playing) return; const container = containers[i]; // 跳过已完成题目 if (AnswerModule._isQuestionCompleted(container)) { Logger.log(`章节测验第 ${i + 1} 题已完成,跳过`, 'info'); continue; } try { const qData = await Solver.recognize(container); if (!qData.question) { Logger.log(`章节测验第 ${i + 1} 题识别失败,跳过`, 'error'); continue; } let result = await AnswerModule._queryAnswer(qData.question, qData.options, qData.type); if (!result.answers || result.answers.length === 0) { Logger.log(`章节测验第 ${i + 1} 题查询无结果,跳过`, 'error'); continue; } await Solver.autoSelectAndSubmit(result.answers, container); Logger.log(`章节测验第 ${i + 1} 题已作答`, 'success'); } catch (e) { Logger.log(`章节测验第 ${i + 1} 题异常: ${e.message || e}`, 'error'); } // 题间延迟(AI模式15秒,其他1-2秒) if (i < containers.length - 1) { const delay = quizMode === 'ai' ? (15000 + Math.random() * 2000) : (1000 + Math.random() * 1000); if (quizMode === 'ai') Logger.log(`AI模式冷却中,${Math.round(delay/1000)}秒后继续...`, 'info'); await new Promise(r => setTimeout(r, delay)); } } // 尝试点击提交按钮 const submitSelectors = [ '.jb_btn.jb_btn_92', 'button.saveWork', '.btnSubmit', 'a.jb_btn', '#submitBtn', '.Btn_blue_1' ]; for (const sel of submitSelectors) { const btn = quizDoc.querySelector(sel); if (btn) { btn.click(); Logger.log('章节测验已提交', 'success'); break; } } // 答完推进下一章,恢复之前的答题模式 AnswerModule._mode = prevMode; if (this._playing) { Logger.log('章节测验完成,推进下一章节', 'info'); await new Promise(r => setTimeout(r, 3000)); this.advanceToNextChapter(); } }, // 内部:绑定视频事件 _bindVideoEvents(video) { // 监听视频结束事件 const onEnded = () => { if (!this._playing) return; Logger.log('当前视频播放完毕', 'success'); // 清除之前的待执行播放定时器 if (this._pendingPlayTimeout) { clearTimeout(this._pendingPlayTimeout); this._pendingPlayTimeout = null; } const cfg = ConfigStore.getVideoConfig(); if (!cfg.autoAdvance) { Logger.log('自动推进已关闭,等待手动操作', 'info'); UIPanel.setStatus('视频播放完毕', 'info'); return; } // 检查当前章节是否还有测验任务点(同一章节下的下一个节点) const autoAnswer = cfg.autoAnswerQuiz !== false; if (autoAnswer) { setTimeout(() => this._checkAndHandleChapterQuiz(), 1500); } else { setTimeout(() => this.advanceToNextChapter(), 1500); } }; video.addEventListener('ended', onEnded); // 保存引用以便清理 this._antiPauseHandlers.push({ target: video, event: 'ended', handler: onEnded, capture: false }); }, // 6.1 - setPlaybackRate(): 设置倍速 setPlaybackRate(rate) { const video = this._videoEl || this._findVideoElement(); if (video) { video.playbackRate = rate; Logger.log(`播放倍速已设置为 ${rate}x`, 'info'); } // 同步到配置 const cfg = ConfigStore.getVideoConfig(); cfg.playbackRate = rate; ConfigStore.setVideoConfig(cfg); }, // 6.2 - installAntiPause(): 安装防暂停钩子 installAntiPause() { this.removeAntiPause(); // 拦截 visibilitychange(capture阶段阻止传播) const onVisibilityChange = (e) => { e.stopImmediatePropagation(); e.preventDefault(); }; document.addEventListener('visibilitychange', onVisibilityChange, true); this._antiPauseHandlers.push({ target: document, event: 'visibilitychange', handler: onVisibilityChange, capture: true }); // 拦截 mouseout const onMouseOut = (e) => { if (this._playing) { e.stopImmediatePropagation(); } }; document.addEventListener('mouseout', onMouseOut, true); this._antiPauseHandlers.push({ target: document, event: 'mouseout', handler: onMouseOut, capture: true }); // 拦截 blur const onBlur = (e) => { if (this._playing) { e.stopImmediatePropagation(); } }; window.addEventListener('blur', onBlur, true); this._antiPauseHandlers.push({ target: window, event: 'blur', handler: onBlur, capture: true }); // 监听视频 pause 事件 — 非用户主动暂停时自动恢复 this._installVideoPauseHook(); // 3秒超时自动恢复:定时检查视频状态 this._pauseCheckInterval = setInterval(() => { if (!this._playing) return; const video = this._videoEl || this._findVideoElement(); if (video && video.paused && !video.ended && !this._userPaused) { const pauseDuration = Date.now() - this._lastPauseTime; if (this._lastPauseTime > 0 && pauseDuration >= 3000) { Logger.log('视频暂停超过3秒,自动恢复播放', 'info'); video.play().catch(() => {}); this._lastPauseTime = 0; } } }, 1000); Logger.log('防中断机制已安装', 'info'); }, // 内部:安装视频pause事件钩子 _installVideoPauseHook() { const hookPause = (video) => { const onPause = () => { if (!this._playing || video.ended) return; if (this._userPaused) return; this._lastPauseTime = Date.now(); // 短暂延迟后恢复,避免与正常seek冲突 setTimeout(() => { if (this._playing && video.paused && !video.ended && !this._userPaused) { video.play().catch(() => {}); } }, 500); }; video.addEventListener('pause', onPause); this._antiPauseHandlers.push({ target: video, event: 'pause', handler: onPause, capture: false }); }; // 对当前视频安装 const video = this._videoEl || this._findVideoElement(); if (video) hookPause(video); }, // 6.2 - removeAntiPause(): 移除所有钩子 removeAntiPause() { this._antiPauseHandlers.forEach(({ target, event, handler, capture }) => { try { target.removeEventListener(event, handler, capture); } catch (_) {} }); this._antiPauseHandlers = []; if (this._pauseCheckInterval) { clearInterval(this._pauseCheckInterval); this._pauseCheckInterval = null; } this._lastPauseTime = 0; }, // 6.3 - _checkAndHandleChapterQuiz(): 视频播完后检查当前页面是否有章节测验iframe async _checkAndHandleChapterQuiz() { if (!this._playing) return; // 学习通的章节页面结构:一个章节下有多个iframe任务点(视频、测验等) // 视频播完后,测验iframe可能已经在当前页面上了 Logger.log('检查当前页面是否有章节测验...', 'info'); // 等待一下让页面稳定 await new Promise(r => setTimeout(r, 1000)); if (!this._playing) return; const quizDoc = this._findChapterQuizDoc(); if (quizDoc) { Logger.log('在当前页面检测到章节测验,开始答题', 'info'); await this._autoAnswerChapterQuiz(quizDoc); return; // _autoAnswerChapterQuiz 完成后会自己调 advanceToNextChapter } // 当前页面没有测验,尝试查找页面上的测验tab/任务点链接并点击 // 学习通页面上方有任务点tab,如 "1 视频" "2 章节测验" const tabSelectors = [ '.prev_ul li', '.ans-attach-ct .ans-job-icon', '.prev_tab li', '.chapter_tab li', '.ans-cc-tab li', '[class*="tab"] li' ]; let quizTab = null; // 在主页面查找 for (const sel of tabSelectors) { const tabs = document.querySelectorAll(sel); for (const tab of tabs) { const text = tab.textContent?.trim() || ''; if (/测验|作业|quiz|test/i.test(text)) { quizTab = tab; break; } } if (quizTab) break; } // 在iframe中查找 if (!quizTab) { const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (!iframeDoc) continue; for (const sel of tabSelectors) { const tabs = iframeDoc.querySelectorAll(sel); for (const tab of tabs) { const text = tab.textContent?.trim() || ''; if (/测验|作业|quiz|test/i.test(text)) { quizTab = tab; break; } } if (quizTab) break; } } catch (_) {} if (quizTab) break; } } if (quizTab) { const tabText = quizTab.textContent?.trim().substring(0, 20) || '测验'; Logger.log(`找到测验tab: ${tabText},点击进入`, 'info'); const clickTarget = quizTab.querySelector('a, span, div') || quizTab; clickTarget.click(); // 多次尝试检测测验,每次等待递增(iframe加载可能较慢) for (let attempt = 1; attempt <= 5; attempt++) { await new Promise(r => setTimeout(r, 2000 + attempt * 1000)); if (!this._playing) return; const quizDoc2 = this._findChapterQuizDoc(); if (quizDoc2) { Logger.log('测验页面已加载,开始答题', 'info'); await this._autoAnswerChapterQuiz(quizDoc2); return; } } Logger.log('多次尝试后仍未检测到题目,推进下一章', 'info'); } else { Logger.log('当前章节无测验,推进下一章', 'info'); } this.advanceToNextChapter(); }, // 6.3 - advanceToNextChapter(): 推进到下一章节 advanceToNextChapter() { if (!this._playing) return; const cfg = ConfigStore.getVideoConfig(); if (!cfg.autoAdvance) { Logger.log('自动推进已关闭', 'info'); return; } // 清除之前的待执行播放定时器,防止重复调用 if (this._pendingPlayTimeout) { clearTimeout(this._pendingPlayTimeout); this._pendingPlayTimeout = null; } const courseTree = document.getElementById('coursetree'); if (!courseTree) { Logger.log('未找到章节树 (#coursetree)', 'error'); return; } // 学习通章节树结构: // #coursetree 下有多个
作为章节分组 // 每个分组下有