// ==UserScript== // @name 学习通刷课助手 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 学习通座位预约+视频自动播放+自动答题一站式助手 // @author 叶屿 // @license GPL3 // @antifeature payment 题库答题功能需要验证码(免费)或激活码(付费),视频播放等基础功能完全免费 // @match *://*.chaoxing.com/* // @match *://office.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 unsafeWindow // @connect office.chaoxing.com // @connect qsy.iano.cn // @connect lyck6.cn // @connect open.bigmodel.cn // @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 // ==/UserScript== /* * ========================================== * 学习通刷课助手 v1.0.0 * ========================================== * * 【功能说明】 * 1. 视频自动播放(支持倍速、自动推进章节、自动跳过测验) * 2. 作业自动答题(题库查询 + AI 答题) * 3. 考试自动答题(支持停止/继续) * 4. 座位预约(定时预约、房间查询) * 5. 双模式题库(免费验证码 / 付费激活码) * * 【积分购买】 * 联系微信:C919irt * 价格表:50积分=2元,100积分=4元,150积分=6元,200积分=8元,500积分=18元 * 说明:每次答题消耗1积分,积分永久有效 * * 【付费声明】 * 本脚本基础功能(视频播放、座位预约)完全免费 * 题库答题功能需要验证码(免费24小时)或激活码(付费永久) * 付费仅用于题库API调用成本,不强制购买 * * 【免责声明】 * 本脚本仅供学习交流使用,请勿用于违反学校规定或作弊行为 * 使用本脚本造成的任何后果由使用者自行承担 * * 【版权信息】 * 作者:叶屿 | 版本:v1.0.0 * * ========================================== */ (() => { 'use strict'; // ========================================== // 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_seat_config'); 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; }, // 座位预约配置 getSeatConfig() { return this.get('cx_seat_config') || {}; }, setSeatConfig(config) { this.set('cx_seat_config', config); }, // 视频播放配置 getVideoConfig() { return this.get('cx_video_config') || { playbackRate: 2, autoAdvance: true, autoSkipQuiz: true }; }, 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-seat-form { display:flex; flex-direction:column; gap:10px; } .cx-seat-session-hint { display:flex; align-items:center; gap:8px; padding:10px 14px; background:linear-gradient(135deg,#eef2ff,#e0e7ff); border-radius:10px; font-size:12px; color:#6366f1; border:1px solid #c7d2fe; } .cx-hint-icon { font-size:16px; } .cx-seat-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:6px; } .cx-seat-actions .cx-btn { font-size:12px; padding:7px 14px; } .cx-seat-msg { font-size:12px; padding:6px 0; min-height:18px; } .cx-seat-msg.cx-msg-success { color:#10b981; } .cx-seat-msg.cx-msg-error { color:#ef4444; } .cx-seat-msg.cx-msg-info { color:#94a3b8; } /* 房间/座位列表 */ .cx-room-list, .cx-seat-list { margin-top:8px; } .cx-room-list-title, .cx-seat-list-title { font-size:12px; font-weight:700; color:#1e293b; margin-bottom:6px; } .cx-room-item { padding:10px 12px; background:#fff; border-radius:10px; margin-bottom:4px; cursor:pointer; font-size:12px; transition:all .2s; display:flex; justify-content:space-between; border:1px solid #e2e8f0; } .cx-room-item:hover { background:#eef2ff; border-color:#c7d2fe; transform:translateX(2px); } .cx-seat-grid { display:flex; flex-wrap:wrap; gap:6px; } .cx-seat-item { width:48px; height:36px; display:flex; align-items:center; justify-content:center; border-radius:8px; font-size:11px; cursor:pointer; transition:all .2s; border:1.5px solid #e2e8f0; font-weight:600; } .cx-seat-item.available { background:#ecfdf5; color:#10b981; border-color:#a7f3d0; } .cx-seat-item.available:hover { background:#10b981; color:#fff; transform:scale(1.05); } .cx-seat-item.occupied { background:#fef2f2; color:#ef4444; border-color:#fecaca; cursor:not-allowed; } .cx-seat-item.reserved { background:#fffbeb; color:#f59e0b; border-color:#fde68a; cursor:not-allowed; } /* 加载动画 */ .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; ConfigStore.setVideoConfig({ playbackRate, autoAdvance, autoSkipQuiz }); // 立即更新当前视频速度 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 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'); }); // 二维码图片加载失败时显示文字占位 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 📋'; }); } }); } } }; // LoginService 已移除 — 座位预约直接使用浏览器已登录的会话 Cookie // ========================================== // SeatModule - 座位预约模块 // ========================================== const SeatModule = { _scheduleTimer: null, _countdownInterval: null, _scheduledTime: null, _retryCount: 0, // 4.2 - 加载配置到表单 loadConfig() { const doc = UIPanel._doc; if (!doc) return; const cfg = ConfigStore.getSeatConfig(); if (cfg.roomId) doc.getElementById('cx-seat-roomid').value = cfg.roomId; if (cfg.seatId) doc.getElementById('cx-seat-seatid').value = cfg.seatId; if (cfg.date) doc.getElementById('cx-seat-date').value = cfg.date; if (cfg.startTime) doc.getElementById('cx-seat-start').value = cfg.startTime; if (cfg.endTime) doc.getElementById('cx-seat-end').value = cfg.endTime; if (cfg.scheduledTime) doc.getElementById('cx-seat-schedule').value = cfg.scheduledTime; }, // 4.2 - 保存配置 saveConfig() { const doc = UIPanel._doc; if (!doc) return false; const roomId = doc.getElementById('cx-seat-roomid').value.trim(); const seatId = doc.getElementById('cx-seat-seatid').value.trim(); const date = doc.getElementById('cx-seat-date').value; const startTime = doc.getElementById('cx-seat-start').value; const endTime = doc.getElementById('cx-seat-end').value; const scheduledTime = doc.getElementById('cx-seat-schedule').value; const config = { roomId, seatId, date, startTime, endTime, scheduledTime }; ConfigStore.setSeatConfig(config); Logger.log('座位预约配置已保存', 'success'); this._showMsg('配置已保存', 'success'); return true; }, // 4.2 - 验证配置 validateConfig() { const doc = UIPanel._doc; if (!doc) return { valid: false, errors: ['面板未初始化'] }; const errors = []; const fields = [ { id: 'cx-seat-roomid', errId: 'cx-seat-roomid-err', label: '房间ID' }, { id: 'cx-seat-seatid', errId: 'cx-seat-seatid-err', label: '座位ID' } ]; let valid = true; fields.forEach(f => { const input = doc.getElementById(f.id); const errEl = doc.getElementById(f.errId); if (!input.value.trim()) { valid = false; errors.push(`${f.label}不能为空`); if (errEl) errEl.textContent = `请填写${f.label}`; input.classList.add('cx-input-error'); } else { if (errEl) errEl.textContent = ''; input.classList.remove('cx-input-error'); } }); return { valid, errors }; }, // 4.3 - 获取 enc_token(使用浏览器已登录的会话) getEncToken() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://office.chaoxing.com/data/apps/seat/reserve', onload: (resp) => { try { const match = resp.responseText.match(/enc\s*[=:]\s*["']([^"']+)["']/); if (match && match[1]) { resolve(match[1]); } else { try { const data = JSON.parse(resp.responseText); resolve(data.enc || data.data?.enc || ''); } catch (_) { resolve(''); } } } catch (e) { resolve(''); } }, onerror: () => resolve('') }); }); }, // 4.3 - 立即预约(使用浏览器会话,无需登录) async reserve() { const validation = this.validateConfig(); if (!validation.valid) { this._showMsg('请填写所有必填字段', 'error'); return { success: false, message: validation.errors.join(', ') }; } const doc = UIPanel._doc; const reserveBtn = doc?.getElementById('cx-btn-reserve'); if (reserveBtn) { reserveBtn.disabled = true; reserveBtn._origText = reserveBtn.textContent; reserveBtn.textContent = '预约中...'; reserveBtn.classList.add('cx-loading'); } try { // Step 1: 获取 enc_token this._showMsg('正在获取令牌...', 'info'); const enc = await this.getEncToken(); // Step 2: 提交预约 this._showMsg('正在提交预约...', 'info'); const roomId = doc.getElementById('cx-seat-roomid').value.trim(); const seatId = doc.getElementById('cx-seat-seatid').value.trim(); const date = doc.getElementById('cx-seat-date').value; const startTime = doc.getElementById('cx-seat-start').value; const endTime = doc.getElementById('cx-seat-end').value; const result = await this._submitReserve(roomId, seatId, date, startTime, endTime, enc); return result; } catch (e) { const msg = `预约异常: ${e.message || e}`; this._showMsg(msg, 'error'); Logger.log(msg, 'error'); return { success: false, message: msg }; } finally { if (reserveBtn) { reserveBtn.disabled = false; reserveBtn.textContent = reserveBtn._origText || '🎯 立即预约'; reserveBtn.classList.remove('cx-loading'); } } }, // 内部:提交预约请求(使用浏览器会话) _submitReserve(roomId, seatId, date, startTime, endTime, enc) { return new Promise((resolve) => { const params = [ `roomid=${encodeURIComponent(roomId)}`, `seatid=${encodeURIComponent(seatId)}`, `date=${encodeURIComponent(date)}`, `startTime=${encodeURIComponent(startTime)}`, `endTime=${encodeURIComponent(endTime)}`, `enc=${encodeURIComponent(enc)}` ].join('&'); GM_xmlhttpRequest({ method: 'POST', url: 'https://office.chaoxing.com/data/apps/seat/submit', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: params, onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.success) { const msg = `预约成功!座位 ${seatId},时间段 ${startTime}-${endTime}`; this._showMsg(msg, 'success'); Logger.log(msg, 'success'); resolve({ success: true, message: msg }); } else { const msg = data.msg || '预约失败'; this._showMsg(`预约失败: ${msg}`, 'error'); Logger.log(`预约失败: ${msg}`, 'error'); resolve({ success: false, message: msg }); } } catch (e) { this._showMsg('预约响应解析失败', 'error'); Logger.log('预约响应解析失败', 'error'); resolve({ success: false, message: '响应解析失败' }); } }, onerror: () => { this._showMsg('预约请求失败', 'error'); Logger.log('预约请求失败', 'error'); resolve({ success: false, message: '网络请求失败' }); } }); }); }, // 4.4 - 定时预约 scheduleReserve(triggerTime) { if (this._scheduleTimer) { this._showMsg('已有定时任务运行中', 'error'); return; } const now = new Date(); const target = new Date(now); const parts = triggerTime.split(':'); target.setHours(parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2] || 0, 10), 0); // 如果目标时间已过,设为明天 if (target <= now) { target.setDate(target.getDate() + 1); } this._scheduledTime = target; this._retryCount = 0; const delay = target.getTime() - Date.now(); Logger.log(`定时预约已设置,将在 ${target.toLocaleTimeString('zh-CN')} 触发`, 'info'); this._showMsg(`定时预约已设置,等待触发...`, 'info'); const doc = UIPanel._doc; if (doc) { doc.getElementById('cx-btn-cancel-schedule').style.display = ''; doc.getElementById('cx-btn-schedule').disabled = true; } // 倒计时显示 this._countdownInterval = setInterval(() => { const remaining = Math.max(0, Math.floor((this._scheduledTime.getTime() - Date.now()) / 1000)); UIPanel.setCountdown(remaining); if (remaining <= 0) { clearInterval(this._countdownInterval); this._countdownInterval = null; } }, 1000); // 定时触发 this._scheduleTimer = setTimeout(() => this._executeScheduledReserve(), delay); }, // 内部:执行定时预约(含重试) async _executeScheduledReserve() { this._scheduleTimer = null; Logger.log('定时预约触发,开始执行...', 'info'); const result = await this.reserve(); if (!result.success && this._retryCount < 3) { this._retryCount++; Logger.log(`预约失败,${5}秒后第${this._retryCount}次重试...`, 'error'); this._showMsg(`预约失败,5秒后重试 (${this._retryCount}/3)`, 'error'); this._scheduleTimer = setTimeout(() => this._executeScheduledReserve(), 5000); } else { // 完成(成功或重试耗尽) this._cleanupSchedule(); if (!result.success) { Logger.log('定时预约失败,已达最大重试次数', 'error'); this._showMsg('定时预约失败,已达最大重试次数', 'error'); } } }, // 4.4 - 取消定时 cancelSchedule() { if (this._scheduleTimer) { clearTimeout(this._scheduleTimer); } this._cleanupSchedule(); Logger.log('定时预约已取消', 'info'); this._showMsg('定时预约已取消', 'info'); }, // 内部:清理定时任务状态 _cleanupSchedule() { if (this._scheduleTimer) { clearTimeout(this._scheduleTimer); this._scheduleTimer = null; } if (this._countdownInterval) { clearInterval(this._countdownInterval); this._countdownInterval = null; } this._scheduledTime = null; this._retryCount = 0; UIPanel.setCountdown(0); const doc = UIPanel._doc; if (doc) { doc.getElementById('cx-btn-cancel-schedule').style.display = 'none'; doc.getElementById('cx-btn-schedule').disabled = false; } }, // 4.4 - 获取倒计时 getCountdown() { if (!this._scheduledTime) return 0; return Math.max(0, Math.floor((this._scheduledTime.getTime() - Date.now()) / 1000)); }, // 4.5 - 查询房间列表 queryRooms() { return new Promise((resolve) => { this._showMsg('正在查询房间...', 'info'); GM_xmlhttpRequest({ method: 'GET', url: 'https://office.chaoxing.com/data/apps/seat/room/list', onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.success && data.data) { const rooms = data.data; this._renderRoomList(rooms); Logger.log(`查询到 ${rooms.length} 个房间`, 'success'); this._showMsg(`查询到 ${rooms.length} 个房间`, 'success'); resolve(rooms); } else { this._showMsg('查询失败,请稍后重试', 'error'); Logger.log('房间查询失败', 'error'); resolve([]); } } catch (e) { this._showMsg('查询失败,请稍后重试', 'error'); Logger.log('房间查询响应解析失败', 'error'); resolve([]); } }, onerror: () => { this._showMsg('查询失败,请稍后重试', 'error'); Logger.log('房间查询请求失败', 'error'); resolve([]); } }); }); }, // 4.5 - 查询座位列表 querySeats(roomId) { return new Promise((resolve) => { this._showMsg('正在加载座位...', 'info'); GM_xmlhttpRequest({ method: 'GET', url: `https://office.chaoxing.com/data/apps/seat/room/seats?roomId=${encodeURIComponent(roomId)}`, onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.success && data.data) { const seats = data.data; this._renderSeatList(seats, roomId); Logger.log(`房间 ${roomId} 有 ${seats.length} 个座位`, 'success'); resolve(seats); } else { this._showMsg('查询失败,请稍后重试', 'error'); Logger.log('座位查询失败', 'error'); resolve([]); } } catch (e) { this._showMsg('查询失败,请稍后重试', 'error'); Logger.log('座位查询响应解析失败', 'error'); resolve([]); } }, onerror: () => { this._showMsg('查询失败,请稍后重试', 'error'); Logger.log('座位查询请求失败', 'error'); resolve([]); } }); }); }, // 内部:渲染房间列表 _renderRoomList(rooms) { const doc = UIPanel._doc; if (!doc) return; const container = doc.getElementById('cx-room-list'); container.style.display = 'block'; container.innerHTML = '
📋 房间列表
'; if (rooms.length === 0) { container.innerHTML += '
暂无可用房间
'; return; } rooms.forEach(room => { const div = doc.createElement('div'); div.className = 'cx-room-item'; div.innerHTML = `${room.name || room.id}可用: ${room.available ?? '?'}/${room.capacity ?? '?'}`; div.addEventListener('click', () => { doc.getElementById('cx-seat-roomid').value = room.id; this.querySeats(room.id); }); container.appendChild(div); }); }, // 内部:渲染座位列表 _renderSeatList(seats, roomId) { const doc = UIPanel._doc; if (!doc) return; const container = doc.getElementById('cx-seat-list'); container.style.display = 'block'; container.innerHTML = '
💺 座位列表
'; const grid = doc.getElementById('cx-seat-grid'); seats.forEach(seat => { const div = doc.createElement('div'); const statusClass = seat.status || 'available'; div.className = `cx-seat-item ${statusClass}`; div.textContent = seat.name || seat.id; div.title = `座位 ${seat.name || seat.id} - ${statusClass === 'available' ? '可用' : statusClass === 'occupied' ? '已占用' : '已预约'}`; if (statusClass === 'available') { div.addEventListener('click', () => { doc.getElementById('cx-seat-roomid').value = roomId; doc.getElementById('cx-seat-seatid').value = seat.id; this._showMsg(`已选择座位 ${seat.name || seat.id}`, 'success'); Logger.log(`已选择房间 ${roomId} 座位 ${seat.id}`, 'info'); }); } grid.appendChild(div); }); }, // 内部:显示消息 _showMsg(text, type = 'info') { const doc = UIPanel._doc; if (!doc) return; const el = doc.getElementById('cx-seat-msg'); if (!el) return; el.textContent = text; el.className = `cx-seat-msg cx-msg-${type}`; }, // 绑定座位模块事件 bindEvents() { const doc = UIPanel._doc; if (!doc) return; // 保存配置 doc.getElementById('cx-btn-save-config').addEventListener('click', () => this.saveConfig()); // 立即预约 doc.getElementById('cx-btn-reserve').addEventListener('click', () => this.reserve()); // 定时预约 doc.getElementById('cx-btn-schedule').addEventListener('click', () => { const scheduleTime = doc.getElementById('cx-seat-schedule').value; if (!scheduleTime) { this._showMsg('请先设置定时触发时间', 'error'); return; } this.scheduleReserve(scheduleTime); }); // 取消定时 doc.getElementById('cx-btn-cancel-schedule').addEventListener('click', () => this.cancelSchedule()); // 查询房间 doc.getElementById('cx-btn-query-rooms').addEventListener('click', () => this.queryRooms()); } }; // ========================================== // VideoModule - 视频自动播放模块 // ========================================== const VideoModule = { _playing: false, _currentChapter: '', _progress: 0, _videoEl: null, _antiPauseHandlers: [], _pauseCheckInterval: null, _quizCheckInterval: null, _progressInterval: null, _loadRetryCount: 0, _maxLoadRetries: 3, _userPaused: false, _lastPauseTime: 0, // 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; } 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; 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; } // 视频未找到,等待加载 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._tryPlayVideo(); } else { Logger.log('视频加载失败,已达最大重试次数', 'error'); UIPanel.setStatus('视频加载失败', 'error'); } }, // 内部:查找视频元素(包括iframe内部) _findVideoElement() { // 先在主页面查找 let video = document.querySelector('video'); if (video) return video; // 在iframe中查找 const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (iframeDoc) { video = iframeDoc.querySelector('video'); if (video) return video; // 嵌套iframe const innerIframes = iframeDoc.querySelectorAll('iframe'); for (const inner of innerIframes) { try { const innerDoc = inner.contentDocument || inner.contentWindow?.document; if (innerDoc) { video = innerDoc.querySelector('video'); if (video) return video; } } catch (_) { /* 跨域iframe忽略 */ } } } } catch (_) { /* 跨域iframe忽略 */ } } return null; }, // 内部:刷新当前章节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 (_) { /* 跨域忽略 */ } } }, // 内部:绑定视频事件 _bindVideoEvents(video) { // 监听视频结束事件 const onEnded = () => { if (!this._playing) return; Logger.log('当前视频播放完毕', 'success'); const cfg = ConfigStore.getVideoConfig(); if (cfg.autoAdvance) { setTimeout(() => this.advanceToNextChapter(), 1500); } else { Logger.log('自动推进已关闭,等待手动操作', 'info'); UIPanel.setStatus('视频播放完毕', 'info'); } }; 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 - advanceToNextChapter(): 推进到下一章节 advanceToNextChapter() { if (!this._playing) return; const cfg = ConfigStore.getVideoConfig(); if (!cfg.autoAdvance) { Logger.log('自动推进已关闭', 'info'); return; } const courseTree = document.getElementById('coursetree'); if (!courseTree) { Logger.log('未找到章节树 (#coursetree)', 'error'); return; } // 查找下一个未完成章节 const chapters = courseTree.querySelectorAll('.posCatalog_select, .posCatalog_active, [onclick]'); let foundCurrent = false; let nextChapter = null; for (const node of chapters) { // 检测当前活跃章节 if (node.classList.contains('posCatalog_active') || node.classList.contains('currents')) { foundCurrent = true; continue; } // 找到当前章节后,查找下一个未完成的 if (foundCurrent) { // 检查是否已完成(通常有完成标记class) const isCompleted = node.classList.contains('finished') || node.querySelector('.icon-finish, .roundpoint.done, .catalog_finished'); if (!isCompleted) { nextChapter = node; break; } } } // 备选:如果上面没找到,尝试更通用的选择器 if (!nextChapter) { const allItems = courseTree.querySelectorAll('.chapter_item, .catalog_sbar, li[id]'); foundCurrent = false; for (const item of allItems) { if (item.classList.contains('posCatalog_active') || item.classList.contains('currents') || item.querySelector('.posCatalog_active, .currents')) { foundCurrent = true; continue; } if (foundCurrent) { const isCompleted = item.classList.contains('finished') || item.querySelector('.icon-finish, .roundpoint.done, .catalog_finished'); if (!isCompleted) { nextChapter = item; break; } } } } if (nextChapter) { const chapterName = nextChapter.textContent?.trim().substring(0, 30) || '下一章节'; Logger.log(`推进到: ${chapterName}`, 'info'); this._currentChapter = chapterName; // 模拟点击 const clickTarget = nextChapter.querySelector('a, span[onclick], div[onclick]') || nextChapter; clickTarget.click(); // 等待页面加载后播放新视频 setTimeout(() => { if (this._playing) { this._loadRetryCount = 0; this._tryPlayVideo(); } }, 3000); } else { Logger.log('所有章节已完成或未找到下一章节', 'success'); UIPanel.setStatus('所有章节已完成', 'success'); } this._updateVideoUI(); }, // 6.4 - detectAndCloseQuiz(): 检测并关闭测验弹窗 detectAndCloseQuiz() { if (!this._playing) return false; const cfg = ConfigStore.getVideoConfig(); if (!cfg.autoSkipQuiz) return false; // 在主页面和iframe中查找测验弹窗 const selectors = [ '.ans-job-icon', '.ans-attach-ct', '.popboxes_close', '.ans-videoquiz-close', '#videoquiz-close', '.close_btn', '.ans-job-close' ]; const tryClose = (doc) => { for (const sel of selectors) { const el = doc.querySelector(sel); if (el && el.offsetParent !== null) { // 找到关闭按钮 const closeBtn = el.querySelector('.close_btn, .ans-videoquiz-close, .popboxes_close') || el; closeBtn.click(); Logger.log('已自动关闭章节测验弹窗', 'info'); // 关闭后继续播放 setTimeout(() => { if (this._playing) { const video = this._videoEl || this._findVideoElement(); if (video && video.paused && !video.ended) { video.play().catch(() => {}); } else if (!video || video.ended) { this.advanceToNextChapter(); } } }, 500); return true; } } return false; }; // 主页面 if (tryClose(document)) return true; // iframe中查找 const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (iframeDoc && tryClose(iframeDoc)) return true; } catch (_) {} } return false; }, // 6.6 - getCurrentChapter(): 返回当前章节名称 getCurrentChapter() { return this._currentChapter || '未知章节'; }, // 6.6 - getProgress(): 返回 0-100 进度百分比 getProgress() { return this._progress; }, // isPlaying(): 返回是否正在播放 isPlaying() { return this._playing; }, // 内部:更新章节名称 _updateChapterName() { // 从章节树获取当前活跃章节名称 const courseTree = document.getElementById('coursetree'); if (courseTree) { const active = courseTree.querySelector('.posCatalog_active, .currents, .active'); if (active) { this._currentChapter = active.textContent?.trim().substring(0, 30) || '当前章节'; return; } } // 备选:从页面标题获取 const title = document.title || ''; if (title) { this._currentChapter = title.substring(0, 30); } }, // 内部:更新播放进度 _updateProgress() { const video = this._videoEl || this._findVideoElement(); if (video && video.duration > 0) { this._progress = Math.round((video.currentTime / video.duration) * 100); } else { this._progress = 0; } this._updateVideoUI(); }, // 6.5 & 6.6 - 更新视频标签页UI _updateVideoUI() { const doc = UIPanel._doc; if (!doc) return; const chapterEl = doc.getElementById('cx-video-chapter'); const progressEl = doc.getElementById('cx-video-progress'); const progressBar = doc.getElementById('cx-video-progress-bar'); const startBtn = doc.getElementById('cx-btn-video-start'); const stopBtn = doc.getElementById('cx-btn-video-stop'); if (chapterEl) chapterEl.textContent = this._currentChapter || '未开始'; if (progressEl) progressEl.textContent = `${this._progress}%`; if (progressBar) progressBar.style.width = `${this._progress}%`; if (startBtn) startBtn.disabled = this._playing; if (stopBtn) stopBtn.disabled = !this._playing; }, // 绑定视频模块事件 bindEvents() { const doc = UIPanel._doc; if (!doc) return; // 开始播放 doc.getElementById('cx-btn-video-start')?.addEventListener('click', () => this.start()); // 停止播放 doc.getElementById('cx-btn-video-stop')?.addEventListener('click', () => { this._userPaused = true; this.stop(); }); } }; // ========================================== // Solver - 答案求解器模块(题目识别与答案匹配) // ========================================== const Solver = { // 题目类型常量 TYPE_SINGLE: 0, // 单选 TYPE_MULTI: 1, // 多选 TYPE_FILL: 2, // 填空 TYPE_JUDGE: 3, // 判断 TYPE_OTHER: 4, // 其他 // ---- 8.1 readFromDOM ---- readFromDOM(element) { const result = { question: '', options: [], type: this.TYPE_OTHER }; if (!element) return result; result.type = this._detectType(element); result.question = this._extractQuestion(element); result.options = this._extractOptions(element); return result; }, _detectType(el) { const cls = el.className || ''; // 通过容器 class 判断 if (/singleQuesId|single/i.test(cls)) return this.TYPE_SINGLE; if (/multipleQuesId|multiple|multi/i.test(cls)) return this.TYPE_MULTI; if (/judgementQuesId|judge|panduan/i.test(cls)) return this.TYPE_JUDGE; if (/completionQuesId|fill|tiankong/i.test(cls)) return this.TYPE_FILL; // 通过题目类型指示文本判断 const typeIndicator = el.querySelector('.mark_name, .Zy_TItle .type, .questionType'); const typeText = typeIndicator ? typeIndicator.textContent : (el.textContent || ''); if (/单选|单项选择/.test(typeText)) return this.TYPE_SINGLE; if (/多选|多项选择|不定项/.test(typeText)) return this.TYPE_MULTI; if (/判断|是非/.test(typeText)) return this.TYPE_JUDGE; if (/填空/.test(typeText)) return this.TYPE_FILL; // 通过选项数量和输入框推断 const inputs = el.querySelectorAll('input[type="radio"]'); if (inputs.length > 0) { // 判断题通常只有2个选项(对/错) const opts = this._extractOptions(el); if (inputs.length === 2 && opts.length === 2) { const joined = opts.join(''); if (/[对错√×✓✗TF]/.test(joined) || /正确|错误|true|false/i.test(joined)) { return this.TYPE_JUDGE; } } return this.TYPE_SINGLE; } const checkboxes = el.querySelectorAll('input[type="checkbox"]'); if (checkboxes.length > 0) return this.TYPE_MULTI; const textInputs = el.querySelectorAll('input[type="text"], textarea'); if (textInputs.length > 0) return this.TYPE_FILL; return this.TYPE_OTHER; }, _extractQuestion(el) { // 优先从 .mark_name 或 .Zy_TItle 提取 const selectors = ['.mark_name', '.Zy_TItle', '.questionContent', '.question_title', '.stem']; for (const sel of selectors) { const node = el.querySelector(sel); if (node) { const text = node.textContent.trim(); if (text) return this._cleanQuestionText(text); } } // 备选:取容器内第一段有意义的文本 const firstP = el.querySelector('p, h3, h4, .title'); if (firstP) { const text = firstP.textContent.trim(); if (text) return this._cleanQuestionText(text); } return ''; }, _cleanQuestionText(text) { // 移除题号前缀(如 "1." "【单选题】" 等) return text.replace(/^[\s\d.、\[\]【】()()]+/, '') .replace(/^(单选题|多选题|判断题|填空题|单项选择题|多项选择题)[】\]))]\s*/, '') .trim(); }, _extractOptions(el) { const options = []; // 优先从 .Zy_ulTop li 提取 const liItems = el.querySelectorAll('.Zy_ulTop li, .answerBg li, .option_li, .answer_p'); if (liItems.length > 0) { liItems.forEach(li => { const text = this._cleanOptionText(li.textContent.trim()); if (text) options.push(text); }); if (options.length > 0) return options; } // 备选:从 label 元素提取 const labels = el.querySelectorAll('label.fl, label.option, .answerBg label'); if (labels.length > 0) { labels.forEach(label => { const text = this._cleanOptionText(label.textContent.trim()); if (text) options.push(text); }); if (options.length > 0) return options; } // 再备选:从包含 radio/checkbox 的容器提取 const inputContainers = el.querySelectorAll('.Zy_ulBottom li, .answerList li, div.option'); inputContainers.forEach(container => { const text = this._cleanOptionText(container.textContent.trim()); if (text) options.push(text); }); return options; }, _cleanOptionText(text) { // 移除选项前缀(如 "A." "A、" "A " 等) return text.replace(/^[A-Za-z][.、::\s]\s*/, '').trim(); }, // ---- 8.2 recognizeByOCR ---- async recognizeByOCR(element) { const result = { question: '', options: [], type: this.TYPE_OTHER }; if (!element) return result; // 查找题目区域中的图片 const images = element.querySelectorAll('img'); if (images.length === 0) return result; try { // 识别第一张图片作为题目 const mainImg = images[0]; const ocrResult = await this._ocrImage(mainImg); if (ocrResult) { result.question = ocrResult.trim(); } // 如果有多张图片,后续图片可能是选项 for (let i = 1; i < images.length; i++) { const optText = await this._ocrImage(images[i]); if (optText && optText.trim()) { result.options.push(optText.trim()); } } // 尝试从 DOM 补充类型检测 result.type = this._detectType(element); } catch (e) { Logger.log('OCR 识别失败: ' + (e.message || e), 'error'); } return result; }, async _ocrImage(imgEl) { if (!imgEl || !imgEl.src) return ''; try { // Tesseract.js v2 API if (typeof Tesseract !== 'undefined' && Tesseract.recognize) { const { data } = await Tesseract.recognize(imgEl.src, 'chi_sim'); return data.text || ''; } } catch (e) { Logger.log('Tesseract OCR 错误: ' + (e.message || e), 'error'); } return ''; }, // recognize(): 综合识别(先 DOM,失败则 OCR) async recognize(element) { const domResult = this.readFromDOM(element); if (domResult.question) return domResult; // DOM 提取失败,尝试 OCR Logger.log('DOM 提取失败,尝试 OCR 识别...', 'info'); return await this.recognizeByOCR(element); }, // ---- 8.3 matchAnswer ---- matchAnswer(answers, pageOptions) { if (!answers || answers.length === 0 || !pageOptions || pageOptions.length === 0) return []; const matched = []; for (const answer of answers) { const ans = answer.trim(); if (!ans) continue; // 策略1:判断题特殊处理 const judgeIdx = this._matchJudge(ans); if (judgeIdx >= 0 && judgeIdx < pageOptions.length) { matched.push(judgeIdx); continue; } // 策略2:纯字母答案映射(A/AC/BCD → 索引) const letterIndices = this._matchLetters(ans); if (letterIndices.length > 0) { const valid = letterIndices.filter(i => i < pageOptions.length); matched.push(...valid); continue; } // 策略3:精确匹配 const exactIdx = this._matchExact(ans, pageOptions); if (exactIdx >= 0) { matched.push(exactIdx); continue; } // 策略4:包含匹配 const containIdx = this._matchContain(ans, pageOptions); if (containIdx >= 0) { matched.push(containIdx); continue; } // 策略5:相似度匹配(>60%) const simIdx = this._matchSimilarity(ans, pageOptions); if (simIdx >= 0) { matched.push(simIdx); continue; } } // 去重 return [...new Set(matched)]; }, _matchJudge(ans) { const truePatterns = /^(对|正确|√|✓|T|true|是|right|yes)$/i; const falsePatterns = /^(错|错误|×|✗|F|false|否|wrong|no)$/i; if (truePatterns.test(ans)) return 0; if (falsePatterns.test(ans)) return 1; return -1; }, _matchLetters(ans) { // 仅当答案是纯字母(A-Z)时才匹配 if (!/^[A-Za-z]+$/.test(ans)) return []; // 排除可能是单词的情况(超过4个字母且不全是大写) if (ans.length > 4 && ans !== ans.toUpperCase()) return []; return ans.toUpperCase().split('').map(ch => ch.charCodeAt(0) - 65); }, _matchExact(ans, pageOptions) { const normalized = ans.toLowerCase().trim(); for (let i = 0; i < pageOptions.length; i++) { if (pageOptions[i].toLowerCase().trim() === normalized) return i; } return -1; }, _matchContain(ans, pageOptions) { const normalized = ans.toLowerCase().trim(); // 答案是选项的子串 for (let i = 0; i < pageOptions.length; i++) { const opt = pageOptions[i].toLowerCase().trim(); if (opt.includes(normalized) || normalized.includes(opt)) return i; } return -1; }, _matchSimilarity(ans, pageOptions) { let bestIdx = -1; let bestScore = 0.6; // 阈值 60% for (let i = 0; i < pageOptions.length; i++) { const score = this._similarity(ans, pageOptions[i]); if (score > bestScore) { bestScore = score; bestIdx = i; } } return bestIdx; }, // 字符串相似度(基于编辑距离) _similarity(a, b) { const s1 = a.toLowerCase().trim(); const s2 = b.toLowerCase().trim(); if (s1 === s2) return 1; if (!s1 || !s2) return 0; const maxLen = Math.max(s1.length, s2.length); if (maxLen === 0) return 1; const dist = this._editDistance(s1, s2); return 1 - dist / maxLen; }, _editDistance(a, b) { const m = a.length, n = b.length; const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (a[i - 1] === b[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]); } } } return dp[m][n]; }, // ---- 8.4 autoSelectAndSubmit ---- async autoSelectAndSubmit(answers, container) { if (!container) { Logger.log('无法解析答案:容器元素为空', 'error'); return; } // 提取页面选项文本 const pageOptions = this._extractOptions(container); if (pageOptions.length === 0 && this._detectType(container) !== this.TYPE_FILL) { Logger.log('无法解析答案:未找到页面选项', 'error'); return; } const type = this._detectType(container); // 填空题特殊处理 if (type === this.TYPE_FILL) { this._fillBlanks(answers, container); return; } // 匹配答案 const matchedIndices = this.matchAnswer(answers, pageOptions); if (matchedIndices.length === 0) { Logger.log('无法解析答案:匹配失败,跳过该题', 'error'); return; } // 选中对应选项 this._selectOptions(matchedIndices, container, type); // 点击提交按钮 await this._clickSubmit(container); }, _selectOptions(indices, container, type) { // 获取所有可点击的选项元素 const optionEls = container.querySelectorAll( '.Zy_ulTop li, .answerBg li, .option_li, label.fl, label.option, .answerList li, div.option' ); // 获取所有 radio/checkbox const radios = container.querySelectorAll('input[type="radio"]'); const checkboxes = container.querySelectorAll('input[type="checkbox"]'); const inputs = radios.length > 0 ? radios : checkboxes; indices.forEach(idx => { // 优先通过 input 元素选中 if (idx < inputs.length) { const input = inputs[idx]; if (!input.checked) { input.click(); input.checked = true; // 触发 change 事件 input.dispatchEvent(new Event('change', { bubbles: true })); } } else if (idx < optionEls.length) { // 备选:点击选项容器 optionEls[idx].click(); } }); Logger.log(`已选择选项: ${indices.map(i => String.fromCharCode(65 + i)).join('')}`, 'info'); }, _fillBlanks(answers, container) { const inputs = container.querySelectorAll('input[type="text"], textarea'); answers.forEach((ans, i) => { if (i < inputs.length) { inputs[i].value = ans.trim(); inputs[i].dispatchEvent(new Event('input', { bubbles: true })); inputs[i].dispatchEvent(new Event('change', { bubbles: true })); } }); Logger.log(`已填写 ${Math.min(answers.length, inputs.length)} 个填空`, 'info'); }, async _clickSubmit(container) { // 查找提交按钮 const submitSelectors = [ '.jb_btn', '#submitBtn', 'a.jb_btn', 'button[type="submit"]', '.Btn_blue_01', '.save_btn', '.submit_btn' ]; let submitBtn = null; for (const sel of submitSelectors) { submitBtn = container.querySelector(sel) || document.querySelector(sel); if (submitBtn) break; } if (submitBtn) { // 短暂延迟后点击提交 await new Promise(r => setTimeout(r, 300)); submitBtn.click(); Logger.log('已点击提交按钮', 'info'); } } }; // ========================================== // AnswerModule - 答题模块(三种答题模式) // ========================================== const AnswerModule = { _mode: 'free', // 当前模式: 'free' | 'paid' | 'ai' _answering: false, _stopRequested: false, _currentQuestion: 0, _totalQuestions: 0, // ---- 模式管理 ---- setMode(mode) { this._mode = mode; const cfg = ConfigStore.getAnswerConfig(); cfg.mode = mode; ConfigStore.setAnswerConfig(cfg); this._updateModeUI(); Logger.log(`答题模式已切换为: ${mode === 'free' ? '免费题库' : mode === 'paid' ? '付费题库' : 'AI 答题'}`, 'info'); }, getMode() { return this._mode; }, // ---- 9.1 免费验证码模式 ---- verifyCode(code) { return new Promise((resolve) => { const doc = UIPanel._doc; const verifyBtn = doc?.getElementById('cx-btn-verify'); if (verifyBtn) { verifyBtn.disabled = true; verifyBtn.textContent = '验证中...'; } GM_xmlhttpRequest({ method: 'POST', url: 'https://qsy.iano.cn/index.php?s=/api/code/verify', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `code=${encodeURIComponent(code)}`, onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.code === 1 && data.data && data.data.valid) { const validUntil = data.data.valid_until; const validUntilStr = data.data.valid_until_str || new Date(validUntil * 1000).toLocaleString('zh-CN'); // 保存有效期 const cfg = ConfigStore.getAnswerConfig(); cfg.verifyValidUntil = validUntil; ConfigStore.setAnswerConfig(cfg); Logger.log(`验证成功,有效期至 ${validUntilStr}`, 'success'); this._updateFreeStatus(true, validUntilStr); resolve({ success: true, validUntil, validUntilStr }); } else { const msg = data.msg || '验证码无效或已过期'; Logger.log(`验证失败: ${msg}`, 'error'); this._updateFreeStatus(false, msg); resolve({ success: false, message: msg }); } } catch (e) { Logger.log('验证响应解析失败', 'error'); this._updateFreeStatus(false, '验证响应解析失败'); resolve({ success: false, message: '响应解析失败' }); } finally { if (verifyBtn) { verifyBtn.textContent = '验证'; // 如果验证成功且有效期内,保持禁用 if (!this.isVerifyValid()) { verifyBtn.disabled = false; } } } }, onerror: () => { Logger.log('验证请求失败', 'error'); this._updateFreeStatus(false, '网络请求失败'); if (verifyBtn) { verifyBtn.disabled = false; verifyBtn.textContent = '验证'; } resolve({ success: false, message: '网络请求失败' }); } }); }); }, isVerifyValid() { const cfg = ConfigStore.getAnswerConfig(); const validUntil = cfg.verifyValidUntil || 0; return validUntil > 0 && Date.now() / 1000 < validUntil; }, _updateFreeStatus(success, message) { const doc = UIPanel._doc; if (!doc) return; const statusEl = doc.getElementById('cx-free-status'); const codeInput = doc.getElementById('cx-free-code'); const verifyBtn = doc.getElementById('cx-btn-verify'); if (!statusEl) return; if (success && this.isVerifyValid()) { statusEl.textContent = `✅ 验证成功,有效期至 ${message}`; statusEl.style.color = '#67c23a'; if (codeInput) codeInput.disabled = true; if (verifyBtn) verifyBtn.disabled = true; } else if (success === false) { statusEl.textContent = `❌ ${message}`; statusEl.style.color = '#f56c6c'; if (codeInput) codeInput.disabled = false; if (verifyBtn) verifyBtn.disabled = false; } else { statusEl.textContent = ''; if (codeInput) codeInput.disabled = false; if (verifyBtn) verifyBtn.disabled = false; } }, // 检查免费模式有效期,过期后重新启用输入 _checkFreeExpiry() { if (this._mode !== 'free') return; const cfg = ConfigStore.getAnswerConfig(); const validUntil = cfg.verifyValidUntil || 0; if (validUntil > 0 && Date.now() / 1000 >= validUntil) { // 已过期 cfg.verifyValidUntil = 0; ConfigStore.setAnswerConfig(cfg); this._updateFreeStatus(null); const doc = UIPanel._doc; if (doc) { const statusEl = doc.getElementById('cx-free-status'); if (statusEl) { statusEl.textContent = '⚠️ 验证码已过期,请重新验证'; statusEl.style.color = '#e6a23c'; } } Logger.log('免费模式验证码已过期', 'info'); } }, // ---- 9.2 付费积分模式 ---- activateCode(code) { return new Promise((resolve) => { const doc = UIPanel._doc; const activateBtn = doc?.getElementById('cx-btn-activate'); if (activateBtn) { activateBtn.disabled = true; activateBtn.textContent = '激活中...'; } GM_xmlhttpRequest({ method: 'POST', url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/activate', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ code: code, device_id: ConfigStore.getDeviceId() }), onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.code === 1 && data.data) { const credits = data.data.credits || 0; const remaining = data.data.remaining_credits || 0; const msg = data.data.message || `恭喜您获得 ${credits} 积分`; Logger.log(`激活成功: ${msg}`, 'success'); this._updateCreditsDisplay(remaining); this._showPaidStatus(true, msg); resolve({ success: true, credits, remaining }); } else { const msg = data.msg || '激活码无效或已使用'; Logger.log(`激活失败: ${msg}`, 'error'); this._showPaidStatus(false, msg); resolve({ success: false, message: msg }); } } catch (e) { Logger.log('激活响应解析失败', 'error'); this._showPaidStatus(false, '激活响应解析失败'); resolve({ success: false, message: '响应解析失败' }); } finally { if (activateBtn) { activateBtn.disabled = false; activateBtn.textContent = '激活'; } } }, onerror: () => { Logger.log('激活请求失败', 'error'); this._showPaidStatus(false, '网络请求失败'); if (activateBtn) { activateBtn.disabled = false; activateBtn.textContent = '激活'; } resolve({ success: false, message: '网络请求失败' }); } }); }); }, getCredits() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: `https://qsy.iano.cn/index.php?s=/api/question_bank/credits&device_id=${encodeURIComponent(ConfigStore.getDeviceId())}`, onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.code === 1 && data.data) { const remaining = data.data.remaining_credits || 0; this._updateCreditsDisplay(remaining); resolve(remaining); } else { resolve(0); } } catch (e) { resolve(0); } }, onerror: () => resolve(0) }); }); }, _updateCreditsDisplay(credits) { const doc = UIPanel._doc; if (!doc) return; const el = doc.getElementById('cx-paid-credits'); if (el) el.textContent = credits; }, _showPaidStatus(success, message) { const doc = UIPanel._doc; if (!doc) return; const statusEl = doc.getElementById('cx-paid-status'); if (!statusEl) return; statusEl.textContent = success ? `✅ ${message}` : `❌ ${message}`; statusEl.style.color = success ? '#67c23a' : '#f56c6c'; }, // ---- 9.3 AI 答题模式 ---- setAIApiKey(key) { const cfg = ConfigStore.getAnswerConfig(); cfg.aiApiKey = key; ConfigStore.setAnswerConfig(cfg); Logger.log('AI API Key 已保存', 'success'); }, askAI(question, options, type) { return new Promise((resolve) => { const cfg = ConfigStore.getAnswerConfig(); const apiKey = cfg.aiApiKey; if (!apiKey) { Logger.log('请先配置 AI API Key', 'error'); resolve({ answers: [], remaining: -1 }); return; } // 构造 prompt const prompt = this._buildAIPrompt(question, options, type); GM_xmlhttpRequest({ method: 'POST', url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, data: JSON.stringify({ model: 'glm-4-flash', messages: [{ role: 'user', content: prompt }] }), timeout: 30000, onload: (resp) => { try { if (resp.status === 401) { Logger.log('AI API Key 无效(401 Unauthorized)', 'error'); resolve({ answers: [], remaining: -1 }); return; } if (resp.status === 429) { Logger.log('AI 请求频率超限(429 Too Many Requests)', 'error'); resolve({ answers: [], remaining: -1 }); return; } const data = JSON.parse(resp.responseText); if (data.choices && data.choices.length > 0) { const content = data.choices[0].message?.content || ''; const answers = this._parseAIAnswer(content, type); Logger.log(`AI 返回答案: ${answers.join(', ')}`, 'success'); resolve({ answers, remaining: -1 }); } else { Logger.log('AI 返回内容为空', 'error'); resolve({ answers: [], remaining: -1 }); } } catch (e) { Logger.log('AI 响应解析失败: ' + (e.message || e), 'error'); resolve({ answers: [], remaining: -1 }); } }, onerror: () => { Logger.log('AI 请求失败(网络错误)', 'error'); resolve({ answers: [], remaining: -1 }); }, ontimeout: () => { Logger.log('AI 请求超时', 'error'); resolve({ answers: [], remaining: -1 }); } }); }); }, _buildAIPrompt(question, options, type) { const typeNames = { 0: '单选题', 1: '多选题', 2: '填空题', 3: '判断题', 4: '题目' }; const typeName = typeNames[type] || '题目'; let prompt = `请回答以下${typeName},`; switch (type) { case Solver.TYPE_SINGLE: prompt += '请只回答选项字母(如 A),不要包含其他内容。\n'; break; case Solver.TYPE_MULTI: prompt += '请回答所有正确选项的字母,多个选项之间用 || 分隔(如 A||C||D),不要包含其他内容。\n'; break; case Solver.TYPE_JUDGE: prompt += '请只回答"对"或"错",不要包含其他内容。\n'; break; case Solver.TYPE_FILL: prompt += '请只回答填空内容,多个空之间用 || 分隔,不要包含其他内容。\n'; break; default: prompt += '请直接回答答案,不要包含解释。\n'; } prompt += `\n题目:${question}\n`; if (options && options.length > 0) { prompt += '选项:\n'; options.forEach((opt, i) => { prompt += `${String.fromCharCode(65 + i)}. ${opt}\n`; }); } return prompt; }, _parseAIAnswer(content, type) { if (!content) return []; const text = content.trim(); // 多选题或填空题:用 || 分隔 if (type === Solver.TYPE_MULTI || type === Solver.TYPE_FILL) { if (text.includes('||')) { return text.split('||').map(s => s.trim()).filter(Boolean); } } // 单选题:提取字母 if (type === Solver.TYPE_SINGLE) { const match = text.match(/^[A-Za-z]/); if (match) return [match[0].toUpperCase()]; } // 多选题备选:提取连续字母 if (type === Solver.TYPE_MULTI) { const match = text.match(/[A-Za-z]/g); if (match) return [...new Set(match.map(c => c.toUpperCase()))]; } // 判断题 if (type === Solver.TYPE_JUDGE) { if (/对|正确|√|true|是|right|yes/i.test(text)) return ['对']; if (/错|错误|×|false|否|wrong|no/i.test(text)) return ['错']; } // 默认返回整个文本 return [text]; }, // ---- 答题查询接口(供后续任务使用) ---- askFreeBank(question, options, type) { return new Promise((resolve) => { if (!this.isVerifyValid()) { Logger.log('免费模式验证码已过期,请重新验证', 'error'); resolve({ answers: [], remaining: -1 }); return; } GM_xmlhttpRequest({ method: 'POST', url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/answer', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ device_id: ConfigStore.getDeviceId(), question, options, type, location: '学习通' }), onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.code === 1 && data.data?.result) { resolve({ answers: data.data.result.answers || [], remaining: -1 }); } else { resolve({ answers: [], remaining: -1 }); } } catch (e) { resolve({ answers: [], remaining: -1 }); } }, onerror: () => resolve({ answers: [], remaining: -1 }) }); }); }, askPaidBank(question, options, type) { return new Promise(async (resolve) => { // 检查积分余额 const credits = await this.getCredits(); if (credits <= 0) { Logger.log('积分不足,请充值后再试', 'error'); resolve({ answers: [], remaining: 0 }); return; } GM_xmlhttpRequest({ method: 'POST', url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/answer', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ device_id: ConfigStore.getDeviceId(), question, options, type, location: '学习通' }), onload: (resp) => { try { const data = JSON.parse(resp.responseText); if (data.code === 1 && data.data?.result) { const remaining = data.data.remaining_credits || 0; this._updateCreditsDisplay(remaining); resolve({ answers: data.data.result.answers || [], remaining }); } else { resolve({ answers: [], remaining: credits }); } } catch (e) { resolve({ answers: [], remaining: credits }); } }, onerror: () => resolve({ answers: [], remaining: credits }) }); }); }, // ---- 10.1 / 10.2 答题查询路由 ---- async _queryAnswer(question, options, type) { switch (this._mode) { case 'free': return this.askFreeBank(question, options, type); case 'paid': return this.askPaidBank(question, options, type); case 'ai': return this.askAI(question, options, type); default: return { answers: [], remaining: -1 }; } }, // ---- 10.1 作业自动答题 ---- async startHomework() { if (this._answering) { Logger.log('答题任务正在进行中', 'info'); return; } // 查找所有题目容器 const containers = document.querySelectorAll( '.questionLi, .singleQuesId, .multipleQuesId, .judgementQuesId, .completionQuesId' ); if (containers.length === 0) { Logger.log('未检测到题目,请确认当前页面为作业页面', 'error'); return; } this._answering = true; this._stopRequested = false; this._totalQuestions = containers.length; this._currentQuestion = 0; UIPanel.setRunning(true); UIPanel.setProgress(0, this._totalQuestions); Logger.log(`检测到 ${this._totalQuestions} 道题目,开始答题...`, 'info'); // 更新按钮状态 this._setAnswerButtonsRunning(true); for (let i = 0; i < containers.length; i++) { if (this._stopRequested) { Logger.log('答题已停止', 'info'); break; } this._currentQuestion = i + 1; UIPanel.setProgress(this._currentQuestion, this._totalQuestions); Logger.log(`正在处理第 ${this._currentQuestion}/${this._totalQuestions} 题`, 'info'); const container = containers[i]; // 跳过已完成题目 if (this._isQuestionCompleted(container)) { Logger.log(`第 ${this._currentQuestion} 题已完成,跳过`, 'info'); continue; } try { // 识别题目 const qData = await Solver.recognize(container); if (!qData.question) { Logger.log(`第 ${this._currentQuestion} 题识别失败,跳过`, 'error'); continue; } // 查询答案(失败后 2 秒重试一次) let result = await this._queryAnswer(qData.question, qData.options, qData.type); if (!result.answers || result.answers.length === 0) { Logger.log(`第 ${this._currentQuestion} 题首次查询失败,2 秒后重试...`, 'info'); await this._delay(2000); result = await this._queryAnswer(qData.question, qData.options, qData.type); } if (!result.answers || result.answers.length === 0) { Logger.log(`第 ${this._currentQuestion} 题查询无结果,跳过`, 'error'); continue; } // 匹配并选择答案 await Solver.autoSelectAndSubmit(result.answers, container); Logger.log(`第 ${this._currentQuestion} 题已作答`, 'success'); } catch (e) { Logger.log(`第 ${this._currentQuestion} 题处理异常: ${e.message || e}`, 'error'); } // 题间延迟 1-2 秒,避免检测 if (i < containers.length - 1 && !this._stopRequested) { await this._delay(1000 + Math.random() * 1000); } } this._answering = false; this._stopRequested = false; UIPanel.setRunning(false); this._setAnswerButtonsRunning(false); if (!this._stopRequested) { Logger.log('答题完成,请检查并提交', 'success'); } }, // ---- 10.2 考试自动答题 ---- async startExam() { if (this._answering) { Logger.log('答题任务正在进行中', 'info'); return; } this._answering = true; this._stopRequested = false; UIPanel.setRunning(true); this._setAnswerButtonsRunning(true); // 检测考试页面布局 const isPaginated = this._isExamPaginated(); Logger.log(`检测到考试布局: ${isPaginated ? '分页模式' : '列表模式'}`, 'info'); try { if (isPaginated) { await this._processExamPaginated(); } else { await this._processExamList(); } } catch (e) { Logger.log(`考试答题异常: ${e.message || e}`, 'error'); } this._answering = false; this._stopRequested = false; UIPanel.setRunning(false); this._setAnswerButtonsRunning(false); Logger.log('答题完成,请检查并提交', 'success'); }, // 检测是否为分页模式考试 _isExamPaginated() { // 分页模式:有侧边栏题号导航 const sidebar = document.querySelector('.exam-sidebar, .leftItems, .questionList, .TiMu-list'); const navItems = sidebar ? sidebar.querySelectorAll('a, li, .item, .TiMu-item') : []; return navItems.length > 0; }, // 分页模式:通过侧边栏题号导航逐题处理 async _processExamPaginated() { const sidebar = document.querySelector('.exam-sidebar, .leftItems, .questionList, .TiMu-list'); if (!sidebar) { Logger.log('未找到侧边栏导航', 'error'); return; } const navItems = sidebar.querySelectorAll('a, li, .item, .TiMu-item'); this._totalQuestions = navItems.length; this._currentQuestion = 0; UIPanel.setProgress(0, this._totalQuestions); Logger.log(`检测到 ${this._totalQuestions} 道题目`, 'info'); for (let i = 0; i < navItems.length; i++) { if (this._stopRequested) { Logger.log('答题已停止', 'info'); break; } this._currentQuestion = i + 1; UIPanel.setProgress(this._currentQuestion, this._totalQuestions); const navItem = navItems[i]; // 跳过已作答题目(.is-checked 标记) if (navItem.classList.contains('is-checked') || navItem.querySelector('.is-checked')) { Logger.log(`第 ${this._currentQuestion} 题已作答,跳过`, 'info'); continue; } // 点击导航切换到该题 navItem.click(); await this._delay(800); Logger.log(`正在处理第 ${this._currentQuestion}/${this._totalQuestions} 题`, 'info'); // 获取当前显示的题目容器 const container = this._findCurrentExamQuestion(); if (!container) { Logger.log(`第 ${this._currentQuestion} 题容器未找到,跳过`, 'error'); continue; } await this._processOneQuestion(container, this._currentQuestion); // 题间延迟 if (i < navItems.length - 1 && !this._stopRequested) { await this._delay(1000 + Math.random() * 1000); } } }, // 列表模式:所有题目在同一页面 async _processExamList() { const containers = document.querySelectorAll( '.exam-main .questionLi, .exam-main .singleQuesId, .exam-main .multipleQuesId, ' + '.exam-main .judgementQuesId, .exam-main .completionQuesId, ' + '.questionLi, .singleQuesId, .multipleQuesId, .judgementQuesId, .completionQuesId' ); if (containers.length === 0) { Logger.log('未检测到题目', 'error'); return; } this._totalQuestions = containers.length; this._currentQuestion = 0; UIPanel.setProgress(0, this._totalQuestions); Logger.log(`检测到 ${this._totalQuestions} 道题目`, 'info'); for (let i = 0; i < containers.length; i++) { if (this._stopRequested) { Logger.log('答题已停止', 'info'); break; } this._currentQuestion = i + 1; UIPanel.setProgress(this._currentQuestion, this._totalQuestions); Logger.log(`正在处理第 ${this._currentQuestion}/${this._totalQuestions} 题`, 'info'); const container = containers[i]; // 跳过已作答题目 if (container.classList.contains('is-checked') || container.querySelector('.is-checked')) { Logger.log(`第 ${this._currentQuestion} 题已作答,跳过`, 'info'); continue; } await this._processOneQuestion(container, this._currentQuestion); // 题间延迟 if (i < containers.length - 1 && !this._stopRequested) { await this._delay(1000 + Math.random() * 1000); } } }, // 处理单道题目(考试模式共用) async _processOneQuestion(container, questionNum) { try { const qData = await Solver.recognize(container); if (!qData.question) { Logger.log(`第 ${questionNum} 题识别失败,跳过`, 'error'); return; } // 查询答案(失败后 2 秒重试一次) let result = await this._queryAnswer(qData.question, qData.options, qData.type); if (!result.answers || result.answers.length === 0) { Logger.log(`第 ${questionNum} 题首次查询失败,2 秒后重试...`, 'info'); await this._delay(2000); result = await this._queryAnswer(qData.question, qData.options, qData.type); } if (!result.answers || result.answers.length === 0) { Logger.log(`第 ${questionNum} 题查询无结果,跳过`, 'error'); return; } // 考试模式不自动提交,只选择答案 const pageOptions = Solver._extractOptions(container); const type = Solver._detectType(container); if (type === Solver.TYPE_FILL) { Solver._fillBlanks(result.answers, container); } else { const matchedIndices = Solver.matchAnswer(result.answers, pageOptions); if (matchedIndices.length === 0) { Logger.log(`第 ${questionNum} 题答案匹配失败,跳过`, 'error'); return; } Solver._selectOptions(matchedIndices, container, type); } Logger.log(`第 ${questionNum} 题已作答`, 'success'); } catch (e) { Logger.log(`第 ${questionNum} 题处理异常: ${e.message || e}`, 'error'); } }, // 查找当前显示的考试题目容器 _findCurrentExamQuestion() { // 分页模式下,当前显示的题目 const visible = document.querySelector( '.questionLi:not([style*="display: none"]):not([style*="display:none"]), ' + '.singleQuesId:not([style*="display: none"]), ' + '.multipleQuesId:not([style*="display: none"]), ' + '.judgementQuesId:not([style*="display: none"]), ' + '.completionQuesId:not([style*="display: none"])' ); if (visible) return visible; // 备选:查找 .exam-main 下的活跃题目 const active = document.querySelector('.exam-main .active, .exam-main .current'); if (active) return active; // 再备选:取第一个题目容器 return document.querySelector('.questionLi, .singleQuesId, .multipleQuesId, .judgementQuesId, .completionQuesId'); }, // 检查题目是否已完成 _isQuestionCompleted(container) { if (!container) return false; // 检查 .is-checked 标记 if (container.classList.contains('is-checked') || container.querySelector('.is-checked')) return true; // 检查已选中的选项 const checked = container.querySelector('input[type="radio"]:checked, input[type="checkbox"]:checked'); if (checked) return true; // 检查填空题已填写 const textInputs = container.querySelectorAll('input[type="text"], textarea'); for (const input of textInputs) { if (input.value && input.value.trim()) return true; } return false; }, // 更新答题按钮状态 _setAnswerButtonsRunning(running) { const doc = UIPanel._doc; if (!doc) return; const homeworkBtn = doc.getElementById('cx-btn-start-homework'); const examBtn = doc.getElementById('cx-btn-start-exam'); const stopBtn = doc.getElementById('cx-btn-stop-answer'); if (homeworkBtn) homeworkBtn.disabled = running; if (examBtn) examBtn.disabled = running; if (stopBtn) stopBtn.disabled = !running; }, // 延迟工具 _delay(ms) { return new Promise(r => setTimeout(r, ms)); }, // ---- 9.4 UI 相关 ---- _updateModeUI() { const doc = UIPanel._doc; if (!doc) return; const mode = this._mode; // 更新 radio 选中状态 doc.querySelectorAll('input[name="cx-answer-mode-tab"]').forEach(radio => { radio.checked = radio.value === mode; }); // 显示/隐藏对应配置区域 doc.getElementById('cx-answer-free-area').style.display = mode === 'free' ? 'block' : 'none'; doc.getElementById('cx-answer-paid-area').style.display = mode === 'paid' ? 'block' : 'none'; doc.getElementById('cx-answer-ai-area').style.display = mode === 'ai' ? 'block' : 'none'; }, // 答题状态 isAnswering() { return this._answering; }, getCurrentQuestion() { return this._currentQuestion; }, getTotalQuestions() { return this._totalQuestions; }, stopAnswering() { this._stopRequested = true; }, // 绑定答题模块事件 bindEvents() { const doc = UIPanel._doc; if (!doc) return; // 模式切换(答题标签页内的 radio) doc.querySelectorAll('input[name="cx-answer-mode-tab"]').forEach(radio => { radio.addEventListener('change', (e) => { this.setMode(e.target.value); }); }); // 免费模式:验证按钮 doc.getElementById('cx-btn-verify')?.addEventListener('click', () => { const code = doc.getElementById('cx-free-code')?.value?.trim(); if (!code || code.length !== 4) { this._updateFreeStatus(false, '请输入4位验证码'); return; } this.verifyCode(code); }); // 免费模式:回车键验证 doc.getElementById('cx-free-code')?.addEventListener('keypress', (e) => { if (e.key === 'Enter') doc.getElementById('cx-btn-verify')?.click(); }); // 付费模式:激活按钮 doc.getElementById('cx-btn-activate')?.addEventListener('click', () => { const code = doc.getElementById('cx-paid-code')?.value?.trim(); if (!code) { this._showPaidStatus(false, '请输入激活码'); return; } this.activateCode(code); }); // 付费模式:刷新积分 doc.getElementById('cx-btn-refresh-credits')?.addEventListener('click', () => { this.getCredits(); Logger.log('正在查询积分余额...', 'info'); }); // AI 模式:保存 API Key doc.getElementById('cx-btn-save-aikey')?.addEventListener('click', () => { const key = doc.getElementById('cx-ai-apikey')?.value?.trim(); if (!key) { Logger.log('请输入 API Key', 'error'); return; } this.setAIApiKey(key); const statusEl = doc.getElementById('cx-ai-status'); if (statusEl) { statusEl.textContent = '✅ API Key 已保存'; statusEl.style.color = '#67c23a'; } }); // 同步设置面板中的答题模式 radio doc.querySelectorAll('input[name="cx-answer-mode"]').forEach(radio => { radio.addEventListener('change', (e) => { this.setMode(e.target.value); // 同步答题标签页的 radio const tabRadio = doc.querySelector(`input[name="cx-answer-mode-tab"][value="${e.target.value}"]`); if (tabRadio) tabRadio.checked = true; }); }); // 作业答题按钮 doc.getElementById('cx-btn-start-homework')?.addEventListener('click', () => { this.startHomework(); }); // 考试答题按钮 doc.getElementById('cx-btn-start-exam')?.addEventListener('click', () => { this.startExam(); }); // 停止答题按钮 doc.getElementById('cx-btn-stop-answer')?.addEventListener('click', () => { this.stopAnswering(); Logger.log('正在停止答题(当前题目处理完后停止)...', 'info'); }); }, // 加载配置并恢复 UI 状态 loadConfig() { const cfg = ConfigStore.getAnswerConfig(); this._mode = cfg.mode || 'free'; this._updateModeUI(); // 恢复免费模式状态 if (this.isVerifyValid()) { const validUntil = cfg.verifyValidUntil; const validUntilStr = new Date(validUntil * 1000).toLocaleString('zh-CN'); this._updateFreeStatus(true, validUntilStr); } // 恢复 AI API Key const doc = UIPanel._doc; if (doc && cfg.aiApiKey) { const keyInput = doc.getElementById('cx-ai-apikey'); if (keyInput) keyInput.value = cfg.aiApiKey; } // 同步设置面板中的答题模式 if (doc) { const settingsRadio = doc.querySelector(`input[name="cx-answer-mode"][value="${this._mode}"]`); if (settingsRadio) settingsRadio.checked = true; } // 如果是付费模式,查询积分 if (this._mode === 'paid') { this.getCredits(); } // 定期检查免费模式有效期 setInterval(() => this._checkFreeExpiry(), 60000); } }; // ========================================== // 脚本入口 — 初始化流程 (12.1, 12.2, 12.3) // ========================================== /** * 运行状态管理器 (12.2) * 负责保存/恢复运行状态,控制自动化任务的开始/停止 */ const RunStateManager = { _timerInterval: null, /** 启动模块并保存运行状态 */ startModule(moduleName) { const state = { status: 'running', activeModule: moduleName, lastActiveTime: Date.now(), startTime: Date.now() }; ConfigStore.setRunState(state); UIPanel.setRunning(true); UIPanel.setStatus('运行中', 'success'); }, /** 停止当前模块并清除运行状态(2秒内完成) */ stopModule() { const state = ConfigStore.getRunState(); if (!state || state.status !== 'running') return; const moduleName = state.activeModule; ConfigStore.clearRunState(); // 停止对应模块 if (moduleName === 'video') { VideoModule.stop(); } else if (moduleName === 'answer') { AnswerModule.stopAnswering(); } UIPanel.setRunning(false); UIPanel.setStatus('已停止', 'info'); }, /** 页面加载后检查是否需要自动恢复 (12.2) */ tryAutoResume() { const state = ConfigStore.getRunState(); if (!state || state.status !== 'running') return; const elapsed = Date.now() - (state.lastActiveTime || 0); // 5分钟内的运行状态才自动恢复 if (elapsed > 5 * 60 * 1000) { Logger.log('上次运行状态已过期(超过5分钟),不自动恢复', 'info'); ConfigStore.clearRunState(); return; } const moduleName = state.activeModule; Logger.log(`检测到上次运行状态,自动恢复模块: ${moduleName}`, 'info'); if (moduleName === 'video') { UIPanel.switchTab('video'); // 延迟启动,等待页面完全加载 setTimeout(() => { VideoModule.start(); this.startModule('video'); }, 1500); } else if (moduleName === 'answer') { UIPanel.switchTab('answer'); Logger.log('答题模块需要手动重新启动', 'info'); ConfigStore.clearRunState(); } }, /** 定期更新 lastActiveTime 保持状态活跃 */ keepAlive() { const state = ConfigStore.getRunState(); if (state && state.status === 'running') { state.lastActiveTime = Date.now(); ConfigStore.setRunState(state); } } }; /** * 脚本初始化 (12.1) * 页面匹配 → DOM 就绪 → 读取配置 → 注入 UI → 绑定模块 → 恢复运行状态 */ async function initScript() { const initStart = Date.now(); try { // 1. 页面匹配检查 const url = window.location.href; if (!/chaoxing\.com/i.test(url)) { return; // 非超星页面,不初始化 } // 2. 等待 DOM 就绪 if (!document.body) { await new Promise(resolve => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', resolve, { once: true }); } else { resolve(); } }); } // 3. 读取配置 const seatCfg = ConfigStore.getSeatConfig(); const videoCfg = ConfigStore.getVideoConfig(); const answerCfg = ConfigStore.getAnswerConfig(); Logger.log('学习通刷课助手已加载', 'success'); // 4. 注入 UI 面板 UIPanel.init(); // 5. 绑定所有模块事件并加载配置 SeatModule.bindEvents(); SeatModule.loadConfig(); VideoModule.bindEvents(); AnswerModule.bindEvents(); AnswerModule.loadConfig(); // 加载视频配置到设置面板 const doc = UIPanel._doc; if (doc) { const rateSelect = doc.getElementById('cx-cfg-playback-rate'); if (rateSelect) rateSelect.value = String(videoCfg.playbackRate || 2); const autoAdvance = doc.getElementById('cx-cfg-auto-advance'); if (autoAdvance) autoAdvance.checked = videoCfg.autoAdvance !== false; const autoSkipQuiz = doc.getElementById('cx-cfg-auto-skip-quiz'); if (autoSkipQuiz) autoSkipQuiz.checked = videoCfg.autoSkipQuiz !== false; // 同步内联视频设置控件 const rateInline = doc.getElementById('cx-video-rate-inline'); if (rateInline) rateInline.value = String(videoCfg.playbackRate || 2); const advanceInline = doc.getElementById('cx-video-advance-inline'); if (advanceInline) advanceInline.checked = videoCfg.autoAdvance !== false; const skipQuizInline = doc.getElementById('cx-video-skip-quiz-inline'); if (skipQuizInline) skipQuizInline.checked = videoCfg.autoSkipQuiz !== false; } // 6. 增强视频模块的 start/stop 以集成运行状态管理 (12.2) const _origVideoStart = VideoModule.start.bind(VideoModule); const _origVideoStop = VideoModule.stop.bind(VideoModule); VideoModule.start = function() { _origVideoStart(); if (VideoModule._playing) { RunStateManager.startModule('video'); } }; VideoModule.stop = function() { _origVideoStop(); ConfigStore.clearRunState(); }; // 增强答题模块的 startHomework/startExam/stopAnswering const _origStartHomework = AnswerModule.startHomework.bind(AnswerModule); const _origStartExam = AnswerModule.startExam.bind(AnswerModule); const _origStopAnswering = AnswerModule.stopAnswering.bind(AnswerModule); AnswerModule.startHomework = async function() { await _origStartHomework(); if (AnswerModule._answering) { RunStateManager.startModule('answer'); } }; AnswerModule.startExam = async function() { await _origStartExam(); if (AnswerModule._answering) { RunStateManager.startModule('answer'); } }; AnswerModule.stopAnswering = function() { _origStopAnswering(); ConfigStore.clearRunState(); UIPanel.setRunning(false); UIPanel.setStatus('已停止', 'info'); }; // 7. 定期更新运行状态的 lastActiveTime(每30秒) setInterval(() => RunStateManager.keepAlive(), 30000); // 8. 检查保存的运行状态,决定是否自动恢复 RunStateManager.tryAutoResume(); const elapsed = Date.now() - initStart; Logger.log(`初始化完成,耗时 ${elapsed}ms`, 'success'); } catch (err) { console.error('[学习通刷课助手] 初始化错误:', err); Logger.log('初始化失败: ' + (err.message || String(err)), 'error'); } } // 启动初始化 initScript(); })();