// ==UserScript== // @name AIRE学习通助手 // @namespace https://github.com/AIREPITI/aire // @version 1.0.2 // @description 超星学习通自动刷课 + 题库搜题 + AI大模型答题助手,悬浮控制面板一键操作 // @author AIRE // @license MIT // @homepage https://github.com/AIREPITI/aire // @supportURL https://github.com/AIREPITI/aire/issues // @match *://*.chaoxing.com/* // @match *://*.xuexitong.com/* // @match *://*.edu.cn/* // @match *://mooc1.chaoxing.com/* // @match *://i.mooc.chaoxing.com/* // @match *://passport2.chaoxing.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_notification // @grant unsafeWindow // @run-at document-end // ==/UserScript== (function () { 'use strict'; // 【iframe 处理】学习通用 iframe 拆分功能区域。 // 视频区 iframe → 跳过(由主页面 CoursePlayer 统一管控) // 答题区 iframe → 放行(需要在里面运行 QuizSolver) if (window.self !== window.top) { const href = location.href; if (/answerQuestion2|test\/|exam\/|work\//i.test(href)) { // 答题/测验 iframe → 允许运行(仅启动答题模块) } else { return; // 其他 iframe(视频播放器等)→ 跳过 } } // ============================================================ // 全局常量 // ============================================================ const SCRIPT_NAME = 'AIRE学习通助手'; const SCRIPT_VERSION = '1.0.0'; const PREFIX = 'aire_'; // ============================================================ // 工具函数 // ============================================================ /** * 防抖函数 * @param {Function} fn * @param {number} delay ms * @returns {Function} */ function debounce(fn, delay) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } /** * 节流函数 * @param {Function} fn * @param {number} interval ms * @returns {Function} */ function throttle(fn, interval) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; fn.apply(this, args); } }; } /** * 获取当前时间戳字符串 [HH:MM:SS] * @returns {string} */ function timestamp() { const d = new Date(); return [ String(d.getHours()).padStart(2, '0'), String(d.getMinutes()).padStart(2, '0'), String(d.getSeconds()).padStart(2, '0') ].join(':'); } /** * 安全 JSON 解析 * @param {string} str * @param {*} fallback * @returns {*} */ function safeJSONParse(str, fallback = null) { try { return JSON.parse(str); } catch (e) { return fallback; } } // ============================================================ // EventBus - 轻量发布/订阅事件总线 // ============================================================ const EventBus = (function () { /** @type {Record} */ const _events = {}; return { /** * 订阅事件 * @param {string} event 事件名 * @param {Function} fn 回调函数 * @returns {Function} 取消订阅函数 */ on(event, fn) { if (!_events[event]) { _events[event] = []; } _events[event].push(fn); // 返回取消订阅函数 return () => this.off(event, fn); }, /** * 订阅一次性事件 * @param {string} event * @param {Function} fn */ once(event, fn) { const wrapper = (...args) => { fn(...args); this.off(event, wrapper); }; this.on(event, wrapper); }, /** * 发布事件 * @param {string} event * @param {*} data */ emit(event, data) { const handlers = _events[event]; if (!handlers || handlers.length === 0) return; // 遍历副本,防止回调中修改数组 handlers.slice().forEach(fn => { try { fn(data); } catch (e) { console.error(`[${SCRIPT_NAME}] EventBus emit "${event}" error:`, e); } }); }, /** * 取消订阅 * @param {string} event * @param {Function} [fn] 不传则取消该事件全部订阅 */ off(event, fn) { if (!_events[event]) return; if (!fn) { delete _events[event]; } else { const idx = _events[event].indexOf(fn); if (idx > -1) _events[event].splice(idx, 1); if (_events[event].length === 0) delete _events[event]; } }, /** * 获取所有已注册的事件名 * @returns {string[]} */ events() { return Object.keys(_events); }, // ---------- 预定义事件名 ---------- // 配置变更 CONFIG_CHANGED: 'config:changed', // 日志 LOG: 'log', // 面板 PANEL_TOGGLE: 'panel:toggle', PANEL_MINIMIZE: 'panel:minimize', PANEL_CLOSE: 'panel:close', // 刷课 PLAYBACK_START: 'playback:start', PLAYBACK_PROGRESS: 'playback:progress', PLAYBACK_COMPLETE: 'playback:complete', PLAYBACK_ERROR: 'playback:error', CHAPTER_NEXT: 'chapter:next', COURSE_COMPLETE: 'course:complete', // 答题 QUIZ_DETECTED: 'quiz:detected', QUIZ_SEARCHING: 'quiz:searching', QUIZ_SOLVED: 'quiz:solved', QUIZ_FAILED: 'quiz:failed', QUIZ_SUBMITTED: 'quiz:submitted', // AI AI_START: 'ai:start', AI_SUCCESS: 'ai:success', AI_FAILED: 'ai:failed', // 页面变化 PAGE_CHANGED: 'page:changed' }; })(); // ============================================================ // ConfigStore - 配置持久化存储 // ============================================================ const ConfigStore = (function () { /** 默认配置 */ const defaults = { // ---- 功能开关 ---- /** 自动刷课开关 */ autoPlay: true, /** 自动答题开关 */ autoQuiz: true, /** AI答题开关(大模型fallback) */ aiEnable: true, /** AI联网搜索增强开关 */ aiSearchEnhance: false, // ---- 答题参数 ---- /** 跳过已作答的题目 */ skipAnswered: true, /** 题目间答题间隔(秒) */ answerInterval: 3, /** 模拟人工延迟 */ simulateDelay: true, /** 自动提交答卷 */ autoSubmit: false, // ---- 播放参数 ---- /** 播放倍速: 1 | 1.25 | 1.5 | 2 | 4 | 8 | 16 */ speed: 2, // ---- AI模型配置 ---- /** API Key */ apiKey: '', /** API 基础地址 */ baseURL: 'https://api.deepseek.com', /** 模型名称 */ model: 'deepseek-v4-flash', /** 模型供应商: 'deepseek' | 'openai' | 'custom' */ modelProvider: 'deepseek', // ---- 面板参数 ---- /** 面板X坐标 */ panelX: null, /** 面板Y坐标 */ panelY: null, /** 面板是否最小化 */ panelMinimized: false }; return { /** * 获取配置值 * @param {string} key * @returns {*} */ get(key) { const stored = GM_getValue(PREFIX + key); if (stored !== undefined && stored !== null) { return stored; } return defaults[key]; }, /** * 设置配置值并持久化 * @param {string} key * @param {*} val */ set(key, val) { try { GM_setValue(PREFIX + key, val); } catch (e) { console.error(`[${SCRIPT_NAME}] ConfigStore set "${key}" failed:`, e); } EventBus.emit(EventBus.CONFIG_CHANGED, { key, val }); }, /** * 删除配置项(恢复默认值) * @param {string} key */ remove(key) { GM_deleteValue(PREFIX + key); EventBus.emit(EventBus.CONFIG_CHANGED, { key, val: defaults[key] }); }, /** * 获取所有配置的副本 * @returns {object} */ getAll() { const result = {}; for (const key of Object.keys(defaults)) { result[key] = this.get(key); } return result; }, /** * 重置所有配置为默认值 */ resetAll() { for (const key of Object.keys(defaults)) { this.remove(key); } }, /** * 批量设置 * @param {object} configObj */ setBatch(configObj) { for (const [key, val] of Object.entries(configObj)) { this.set(key, val); } } }; })(); // ============================================================ // 日志管理器 // ============================================================ const Logger = (function () { /** @type {Array<{time: string, type: string, msg: string}>} */ const _logs = []; const MAX_LOGS = 200; return { /** 日志类型 */ TYPE: { INFO: 'info', SUCCESS: 'success', WARN: 'warn', ERROR: 'error', PLAY: 'play', QUIZ: 'quiz', SEARCH: 'search', AI: 'ai' }, /** * 记录日志 * @param {string} msg * @param {string} [type='info'] */ log(msg, type = 'info') { const entry = { time: timestamp(), type, msg: String(msg) }; _logs.unshift(entry); if (_logs.length > MAX_LOGS) { _logs.length = MAX_LOGS; } console.log(`[${SCRIPT_NAME}][${entry.time}][${type}]`, msg); EventBus.emit(EventBus.LOG, entry); }, info(msg) { this.log(msg, this.TYPE.INFO); }, success(msg) { this.log(msg, this.TYPE.SUCCESS); }, warn(msg) { this.log(msg, this.TYPE.WARN); }, error(msg) { this.log(msg, this.TYPE.ERROR); }, play(msg) { this.log(msg, this.TYPE.PLAY); }, quiz(msg) { this.log(msg, this.TYPE.QUIZ); }, search(msg) { this.log(msg, this.TYPE.SEARCH); }, ai(msg) { this.log(msg, this.TYPE.AI); }, /** * 获取日志列表 * @returns {Array} */ getLogs() { return _logs.slice(); }, /** * 清空日志 */ clear() { _logs.length = 0; } }; })(); // ============================================================ // PanelMgr - 悬浮控制面板管理器 // ============================================================ const PanelMgr = (function () { let _panelEl = null; let _minimizedEl = null; let _isMinimized = false; let _isDragging = false; let _dragX = 0, _dragY = 0; let _startX = 0, _startY = 0; let _aiConfigExpanded = false; let _aiCallCount = 0; let _quizSolvedCount = 0; let _quizTotalCount = 0; let _currentCourse = ''; // 日志容器引用 let _logContainer = null; let _quizProgressBar = null; // ========== CSS 注入 (暗色玻璃态主题 v2.0) ========== const PANEL_CSS = ` .airare-overlay * { margin:0; padding:0; box-sizing:border-box; } .airare-panel { position:fixed; z-index:2147483647; width:420px; max-height:620px; background:rgba(255,255,255,0.78); border-radius:18px; border:1px solid rgba(0,0,0,0.08); box-shadow:0 8px 60px rgba(0,0,0,0.12), 0 2px 20px rgba(0,0,0,0.06); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; font-size:13px; color:#1a1a2e; display:flex; flex-direction:column; backdrop-filter:blur(28px) saturate(180%); -webkit-backdrop-filter:blur(28px) saturate(180%); user-select:none; transition:transform .3s cubic-bezier(.4,0,.2,1), opacity .3s; overflow:hidden; } .airare-panel.minimizing { transform:scale(0.35) translateY(-80px); opacity:0; pointer-events:none; } /* 标题栏 */ .airare-titlebar { display:flex; align-items:center; justify-content:space-between; padding:14px 18px; cursor:move; flex-shrink:0; background:rgba(0,0,0,0.03); border-bottom:1px solid rgba(0,0,0,0.06); border-radius:18px 18px 0 0; } .airare-title { font-size:14px; font-weight:700; color:#111; display:flex; align-items:center; gap:10px; letter-spacing:0.3px; } .airare-status-dot { width:9px; height:9px; border-radius:50%; background:#22c55e; box-shadow:0 0 8px rgba(34,197,94,0.4); animation:pulse-dot 2s infinite; } .airare-status-dot.off { background:rgba(0,0,0,0.15); box-shadow:none; animation:none; } @keyframes pulse-dot { 0%,100% { box-shadow:0 0 5px rgba(34,197,94,0.3); } 50% { box-shadow:0 0 12px rgba(34,197,94,0.7); } } .airare-titlebar-btns { display:flex; gap:8px; } .airare-titlebar-btns button { width:28px; height:28px; border:none; border-radius:8px; background:rgba(0,0,0,0.06); color:#666; cursor:pointer; font-size:14px; display:flex; align-items:center; justify-content:center; transition:all .2s; font-weight:600; } .airare-titlebar-btns button:hover { background:rgba(0,0,0,0.12); color:#333; } .airare-titlebar-btns .airare-close:hover { background:rgba(239,68,68,0.15); color:#ef4444; } /* 导航栏 */ .airare-nav { display:flex; flex-shrink:0; background:rgba(0,0,0,0.02); border-bottom:1px solid rgba(0,0,0,0.06); padding:0 4px; } .airare-nav-btn { flex:1; padding:10px 8px; border:none; background:none; font-size:12.5px; font-weight:600; color:#888; cursor:pointer; transition:all .2s; border-bottom:2px solid transparent; position:relative; letter-spacing:0.3px; } .airare-nav-btn:hover { color:#444; background:rgba(0,0,0,0.02); } .airare-nav-btn.active { color:#111; border-bottom-color:#333; } .airare-tab { display:none; } .airare-tab.active { display:block; } /* 主体 */ .airare-body { flex:1; overflow-y:auto; padding:8px 0; display:flex; flex-direction:column; } .airare-body::-webkit-scrollbar { width:4px; } .airare-body::-webkit-scrollbar-thumb { background:rgba(0,0,0,0.1); border-radius:4px; } .airare-body::-webkit-scrollbar-track { background:transparent; } /* 区块卡片 */ .airare-section { margin:4px 14px; padding:12px 14px; background:rgba(0,0,0,0.025); border-radius:14px; border:1px solid rgba(0,0,0,0.05); } .airare-section-title { font-size:11px; font-weight:700; color:#555; text-transform:uppercase; letter-spacing:1px; margin-bottom:10px; padding-bottom:6px; border-bottom:1px solid rgba(0,0,0,0.06); display:flex; align-items:center; gap:6px; } .airare-section-title::before { content:''; width:3px; height:14px; border-radius:3px; background:#333; } /* 开关行 */ .airare-toggle-row { display:flex; align-items:center; justify-content:space-between; padding:7px 0; } .airare-toggle-row + .airare-toggle-row { border-top:1px solid rgba(0,0,0,0.04); } .airare-toggle-label { font-size:13px; color:#333; font-weight:500; } .airare-toggle { position:relative; width:46px; height:26px; flex-shrink:0; } .airare-toggle input { display:none; } .airare-toggle .slider { position:absolute; inset:0; border-radius:26px; background:rgba(0,0,0,0.15); cursor:pointer; transition:all .3s; } .airare-toggle .slider::after { content:''; position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:#fff; transition:all .3s cubic-bezier(.4,0,.2,1); box-shadow:0 1px 3px rgba(0,0,0,0.15); } .airare-toggle input:checked + .slider { background:#333; } .airare-toggle input:checked + .slider::after { transform:translateX(20px); } /* 设置行 */ .airare-setting-row { display:flex; align-items:center; justify-content:space-between; padding:6px 0; font-size:13px; color:#333; } .airare-setting-row + .airare-setting-row { border-top:1px solid rgba(0,0,0,0.04); } .airare-setting-row select { background:rgba(0,0,0,0.04); color:#333; border:1px solid rgba(0,0,0,0.1); border-radius:8px; padding:4px 10px; font-size:12px; outline:none; cursor:pointer; } .airare-setting-row select:focus { border-color:rgba(0,0,0,0.3); } .airare-input { width:100%; padding:9px 12px; margin:5px 0; border-radius:10px; border:1px solid rgba(0,0,0,0.1); background:rgba(0,0,0,0.03); color:#333; font-size:12.5px; outline:none; transition:all .2s; box-sizing:border-box; } .airare-input::placeholder { color:#999; } .airare-input:focus { border-color:rgba(0,0,0,0.35); background:rgba(0,0,0,0.05); } .airare-select { width:100%; padding:9px 12px; margin:5px 0; border-radius:10px; border:1px solid rgba(0,0,0,0.1); background:rgba(0,0,0,0.03); color:#333; font-size:12.5px; outline:none; cursor:pointer; box-sizing:border-box; } /* 测试连接按钮 */ .airare-test-btn { width:100%; margin-top:8px; padding:8px 12px; border-radius:10px; border:1px solid rgba(0,0,0,0.12); background:#333; color:#fff; font-size:12px; cursor:pointer; transition:all .2s; font-weight:500; } .airare-test-btn:hover { background:#555; } .airare-test-btn:disabled { opacity:0.5; cursor:not-allowed; } .airare-test-result { display:inline-block; font-size:11px; padding:3px 8px; border-radius:6px; } .airare-test-result.success { color:#16a34a; } .airare-test-result.fail { color:#dc2626; } /* 状态网格 */ .airare-status-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px 12px; font-size:12px; color:#666; line-height:1.8; } .airare-status-grid strong { color:#111; font-weight:600; background:rgba(0,0,0,0.04); padding:2px 8px; border-radius:5px; } /* 进度条 */ .airare-progress-bar { width:100%; height:4px; border-radius:4px; background:rgba(0,0,0,0.08); margin:6px 0; overflow:hidden; } .airare-progress-fill { height:100%; border-radius:4px; background:#333; transition:width .5s cubic-bezier(.4,0,.2,1); box-shadow:0 0 6px rgba(0,0,0,0.1); } /* 日志区 */ .airare-log-container { flex:1; min-height:120px; max-height:200px; overflow-y:auto; padding:10px 14px; font-size:11px; font-family:"SF Mono","Cascadia Code","Consolas",monospace; background:rgba(0,0,0,0.025); border-radius:10px; margin:0 4px; } .airare-log-container::-webkit-scrollbar { width:3px; } .airare-log-container::-webkit-scrollbar-thumb { background:rgba(0,0,0,0.08); border-radius:3px; } .airare-log-entry { padding:3px 0; border-bottom:1px solid rgba(0,0,0,0.03); display:flex; gap:8px; align-items:flex-start; } .airare-log-time { color:#999; flex-shrink:0; font-size:10px; } .airare-log-type { flex-shrink:0; padding:1px 6px; border-radius:4px; font-size:9px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase; } .airare-log-type.info { color:#666; background:rgba(0,0,0,0.06); } .airare-log-type.success { color:#16a34a; background:rgba(34,197,94,0.08); } .airare-log-type.warn { color:#ca8a04; background:rgba(234,179,8,0.08); } .airare-log-type.error { color:#dc2626; background:rgba(239,68,68,0.08); } .airare-log-type.play { color:#2563eb; background:rgba(59,130,246,0.08); } .airare-log-type.quiz { color:#7c3aed; background:rgba(139,92,246,0.08); } .airare-log-type.search { color:#0369a1; background:rgba(14,165,233,0.08); } .airare-log-type.ai { color:#be185d; background:rgba(236,72,153,0.08); } .airare-log-msg { color:#444; word-break:break-all; line-height:1.4; } /* 页脚 */ .airare-footer { padding:8px 18px; font-size:10px; color:#aaa; border-top:1px solid rgba(0,0,0,0.05); display:flex; justify-content:space-between; letter-spacing:0.3px; } /* 最小化浮动按钮 */ .airare-mini-btn { position:fixed; z-index:2147483646; width:52px; height:52px; border-radius:50%; background:rgba(0,0,0,0.82); border:1px solid rgba(255,255,255,0.2); box-shadow:0 8px 32px rgba(0,0,0,0.2); cursor:pointer; display:none; align-items:center; justify-content:center; font-size:22px; color:#fff; backdrop-filter:blur(24px); -webkit-backdrop-filter:blur(24px); transition:all .25s cubic-bezier(.4,0,.2,1); animation:float-in .4s ease-out; } @keyframes float-in { from { transform:scale(0); opacity:0; } to { transform:scale(1); opacity:1; } } .airare-mini-btn:hover { transform:scale(1.1); box-shadow:0 14px 40px rgba(0,0,0,0.3); } .airare-mini-btn.show { display:flex; } /* 提示文字 */ .airare-hint { font-size:11px; color:#888; background:rgba(0,0,0,0.03); padding:4px 10px; border-radius:6px; margin-top:5px; } `; // ========== DOM 构建 ========== function _buildPanelDOM() { document.querySelectorAll('#aire-helper-root, .airare-overlay, .airare-mini-btn, #airare-main-panel').forEach(el => { try { el.remove(); } catch (e) {} }); _panelEl = null; _minimizedEl = null; _logContainer = null; const cfg = ConfigStore.getAll(); // 主面板 const panel = document.createElement('div'); panel.className = 'airare-panel'; panel.id = 'airare-main-panel'; const px = cfg.panelX, py = cfg.panelY; if (px != null && py != null) { panel.style.left = px + 'px'; panel.style.top = py + 'px'; } else { panel.style.right = '20px'; panel.style.top = '60px'; } // ---- 标题栏 ---- const tb = document.createElement('div'); tb.className = 'airare-titlebar'; tb.innerHTML = `
AIRE 学习通助手
`; panel.appendChild(tb); // ---- 导航栏 ---- const nav = document.createElement('div'); nav.className = 'airare-nav'; const tabs = ['刷课', '答题', 'AI配置', '日志']; tabs.forEach((name, i) => { const btn = document.createElement('button'); btn.className = 'airare-nav-btn' + (i === 0 ? ' active' : ''); btn.textContent = name; btn.dataset.tab = i; nav.appendChild(btn); }); panel.appendChild(nav); // ---- Body ---- const body = document.createElement('div'); body.className = 'airare-body'; // === Tab 0: 刷课 === const tab0 = document.createElement('div'); tab0.className = 'airare-tab active'; tab0.dataset.tab = '0'; const ctrlSec = _buildSection('控制中心'); ctrlSec.appendChild(_buildToggle('自动刷课', 'autoPlay', cfg.autoPlay)); ctrlSec.appendChild(_buildToggle('自动答题', 'autoQuiz', cfg.autoQuiz)); tab0.appendChild(ctrlSec); // 运行状态 const statSec0 = _buildSection('运行状态'); statSec0.innerHTML += `
课程: --
进度: --
已答题: 0
AI调用: 0
任务点: --
`; _quizProgressBar = statSec0.querySelector('#airare-quiz-progress-bar'); tab0.appendChild(statSec0); body.appendChild(tab0); // === Tab 1: 答题 === const tab1 = document.createElement('div'); tab1.className = 'airare-tab'; tab1.dataset.tab = '1'; const quizSetSec = _buildSection('答题设置'); quizSetSec.appendChild(_buildToggle('AI 智能答题', 'aiEnable', cfg.aiEnable)); quizSetSec.innerHTML += `
跳过已答题
答题间隔
模拟延迟
自动提交
`; tab1.appendChild(quizSetSec); body.appendChild(tab1); // === Tab 2: AI配置 === const tab2 = document.createElement('div'); tab2.className = 'airare-tab'; tab2.dataset.tab = '2'; tab2.innerHTML = `
`; body.appendChild(tab2); // === Tab 3: 日志 === const tab3 = document.createElement('div'); tab3.className = 'airare-tab'; tab3.dataset.tab = '3'; const logContainer = document.createElement('div'); logContainer.className = 'airare-log-container'; logContainer.id = 'airare-log-list'; logContainer.innerHTML = '
等待任务...
'; tab3.appendChild(logContainer); body.appendChild(tab3); _logContainer = logContainer; panel.appendChild(body); // ---- 底栏 ---- const footer = document.createElement('div'); footer.className = 'airare-footer'; footer.innerHTML = `v${SCRIPT_VERSION}AIRE`; panel.appendChild(footer); // ---- 最小化按钮 ---- const miniBtn = document.createElement('div'); miniBtn.className = 'airare-mini-btn'; miniBtn.id = 'airare-mini-btn'; miniBtn.innerHTML = '🤖'; miniBtn.title = 'AIRE学习通助手'; // Shadow DOM const rootEl = document.createElement('div'); rootEl.id = 'aire-helper-root'; const shadow = rootEl.attachShadow({ mode: 'open' }); const styleEl = document.createElement('style'); styleEl.textContent = PANEL_CSS; shadow.appendChild(styleEl); shadow.appendChild(panel); shadow.appendChild(miniBtn); document.documentElement.appendChild(rootEl); _panelEl = panel; _minimizedEl = miniBtn; if (cfg.panelMinimized) { minimize(true); } // 导航切换事件 const navBtns = shadow.querySelectorAll('.airare-nav-btn'); const tabEls = shadow.querySelectorAll('.airare-tab'); navBtns.forEach(btn => { btn.addEventListener('click', () => { navBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); tabEls.forEach(t => t.classList.remove('active')); const target = shadow.querySelector('.airare-tab[data-tab="' + btn.dataset.tab + '"]'); if (target) target.classList.add('active'); }); }); } function _buildSection(title) { const sec = document.createElement('div'); sec.className = 'airare-section'; sec.innerHTML = `
${title}
`; return sec; } function _buildToggle(label, configKey, checked) { const row = document.createElement('div'); row.className = 'airare-toggle-row'; const id = 'airare-toggle-' + configKey; row.innerHTML = ` ${label} `; return row; } // ========== 事件绑定 ========== function _bindEvents() { if (!_panelEl) return; // 拖拽 const titlebar = _panelEl.querySelector('.airare-titlebar'); titlebar.addEventListener('mousedown', _onDragStart); // 最小化 const minBtn = _panelEl.querySelector('#airare-min'); minBtn.addEventListener('click', () => minimize()); // 关闭 const closeBtn = _panelEl.querySelector('#airare-close'); closeBtn.addEventListener('click', close); // 最小化按钮点击展开 if (_minimizedEl) { _minimizedEl.addEventListener('click', () => expand()); } // 拖拽 document.addEventListener('mousemove', _onDragMove); document.addEventListener('mouseup', _onDragEnd); // ---- 控制开关 ---- _bindToggle('airare-toggle-autoPlay', 'autoPlay'); _bindToggle('airare-toggle-autoQuiz', 'autoQuiz'); _bindToggle('airare-toggle-aiEnable', 'aiEnable'); // 答题设置 _bindToggle('airare-toggle-skipAnswered', 'skipAnswered'); _bindToggle('airare-toggle-simulateDelay', 'simulateDelay'); _bindToggle('airare-toggle-autoSubmit', 'autoSubmit'); // 答题间隔 const intervalSel = _panelEl.querySelector('#airare-answer-interval'); if (intervalSel) { intervalSel.addEventListener('change', () => { ConfigStore.set('answerInterval', parseInt(intervalSel.value)); }); } // AI 配置输入 _bindInput('airare-apikey', 'apiKey'); _bindInput('airare-baseurl', 'baseURL'); _bindInput('airare-model', 'model'); const providerSel = _panelEl.querySelector('#airare-provider'); if (providerSel) { providerSel.addEventListener('change', () => { ConfigStore.set('modelProvider', providerSel.value); }); } // AI 测试连接按钮 const testBtn = _panelEl.querySelector('#airare-test-conn'); const testResult = _panelEl.querySelector('#airare-test-result'); if (testBtn) { testBtn.addEventListener('click', async () => { testBtn.disabled = true; testBtn.textContent = '⏳ 测试中...'; testResult.textContent = ''; testResult.className = 'airare-test-result'; const apiKey = _panelEl.querySelector('#airare-apikey').value; if (!apiKey) { testResult.textContent = '❌ 请先填写 API Key'; testResult.className = 'airare-test-result fail'; testBtn.disabled = false; testBtn.textContent = '🔗 测试连接'; return; } // 保存到 ConfigStore ConfigStore.set('apiKey', apiKey); try { const result = await AISolver.askAI('1+1=?', 'fill', []); if (result.success) { testResult.textContent = '✅ 连接成功!'; testResult.className = 'airare-test-result success'; Logger.success('AI 模型连接测试通过'); } else { testResult.textContent = '❌ ' + (result.error || '连接失败'); testResult.className = 'airare-test-result fail'; Logger.error('AI 连接测试失败: ' + (result.error || '未知错误')); } } catch (e) { testResult.textContent = '❌ 请求异常: ' + e.message; testResult.className = 'airare-test-result fail'; } testBtn.disabled = false; testBtn.textContent = '🔗 测试连接'; }); } // ---- 答题设置绑定 ---- _bindToggle('airare-toggle-skipAnswered', 'skipAnswered'); _bindToggle('airare-toggle-simulateDelay', 'simulateDelay'); _bindToggle('airare-toggle-autoSubmit', 'autoSubmit'); // Logger 事件 → 面板日志 EventBus.on(EventBus.LOG, (entry) => { _appendLog(entry); }); // 答题计数 EventBus.on(EventBus.QUIZ_SOLVED, () => { _quizSolvedCount++; const el = _panelEl && _panelEl.querySelector('#airare-quiz-count'); if (el) el.textContent = _quizSolvedCount; }); // AI 调用计数 + 任务点状态 EventBus.on(EventBus.AI_SUCCESS, () => { incrementAiCount(); }); EventBus.on(EventBus.QUIZ_SOLVED, () => { const el = _getEl('airare-quiz-count'); _quizSolvedCount++; if (el) el.textContent = _quizSolvedCount; }); EventBus.on(EventBus.CONFIG_CHANGED, (data) => { _syncConfigUI(data.key, data.val); // 更新任务点显示 if (data.key === 'autoPlay' || data.key === 'autoQuiz') _refreshTaskStatus(); }); // 配置变更同步UI EventBus.on(EventBus.CONFIG_CHANGED, ({ key, val }) => { _syncConfigUI(key, val); }); } function _bindToggle(id, configKey) { const el = _panelEl && _panelEl.querySelector('#' + id); if (el) { el.addEventListener('change', () => { ConfigStore.set(configKey, el.checked); Logger.info(`${configKey}: ${el.checked ? '开启' : '关闭'}`); }); } } function _bindInput(id, configKey) { const el = _panelEl && _panelEl.querySelector('#' + id); if (el) { el.addEventListener('change', () => { ConfigStore.set(configKey, el.value); }); // 失焦也保存 el.addEventListener('blur', () => { ConfigStore.set(configKey, el.value); }); } } function _syncConfigUI(key, val) { const map = { 'autoPlay': 'airare-toggle-autoPlay', 'autoQuiz': 'airare-toggle-autoQuiz', 'aiEnable': 'airare-toggle-aiEnable' }; const id = map[key]; if (!id || !_panelEl) return; const el = _panelEl.querySelector('#' + id); if (!el) return; if (el.type === 'checkbox') { el.checked = val; } } // ========== 日志追加 ========== function _appendLog(entry) { if (!_logContainer) return; // 移除空状态 const emptyEl = _logContainer.querySelector('div[style]'); if (emptyEl) emptyEl.remove(); const row = document.createElement('div'); row.className = 'airare-log-entry'; row.innerHTML = ` ${entry.time} ${entry.type.toUpperCase()} ${_escapeHTML(entry.msg)} `; _logContainer.insertBefore(row, _logContainer.firstChild); // 限制最多80条(方便 debug) while (_logContainer.children.length > 80) { _logContainer.removeChild(_logContainer.lastChild); } } function _escapeHTML(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // ========== 拖拽 ========== function _onDragStart(e) { if (e.target.tagName === 'BUTTON') return; _isDragging = true; _startX = e.clientX; _startY = e.clientY; const rect = _panelEl.getBoundingClientRect(); _dragX = rect.left; _dragY = rect.top; _panelEl.style.right = 'auto'; _panelEl.style.left = _dragX + 'px'; _panelEl.style.top = _dragY + 'px'; _panelEl.style.opacity = '0.85'; e.preventDefault(); } function _onDragMove(e) { if (!_isDragging) return; const dx = e.clientX - _startX; const dy = e.clientY - _startY; let nx = _dragX + dx; let ny = _dragY + dy; // 边界限制 const maxX = window.innerWidth - _panelEl.offsetWidth; const maxY = window.innerHeight - 40; nx = Math.max(0, Math.min(nx, maxX)); ny = Math.max(0, Math.min(ny, maxY)); _panelEl.style.left = nx + 'px'; _panelEl.style.top = ny + 'px'; } function _onDragEnd() { if (!_isDragging) return; _isDragging = false; _panelEl.style.opacity = ''; const rect = _panelEl.getBoundingClientRect(); ConfigStore.set('panelX', rect.left); ConfigStore.set('panelY', rect.top); } // ========== 最小化/展开 ========== function minimize(silent = false) { if (_isMinimized) return; if (!silent) { _panelEl.classList.add('minimizing'); } setTimeout(() => { _panelEl.style.display = 'none'; _minimizedEl.style.right = '16px'; _minimizedEl.style.top = '80px'; _minimizedEl.classList.add('show'); }, silent ? 0 : 200); _isMinimized = true; ConfigStore.set('panelMinimized', true); } function expand() { if (!_isMinimized) return; _minimizedEl.classList.remove('show'); _panelEl.style.display = ''; _panelEl.classList.remove('minimizing'); _isMinimized = false; ConfigStore.set('panelMinimized', false); Logger.info('面板已展开'); } function close() { _panelEl.style.display = 'none'; if (_minimizedEl) _minimizedEl.classList.remove('show'); Logger.info('面板已关闭,刷新页面可重新打开'); } // ========== 状态更新 ========== /** 安全获取面板内元素 */ function _getEl(id) { if (!_panelEl) return null; // 先在当前 shadow 容器内查 const root = _panelEl.getRootNode(); let el = root.querySelector('#' + id); if (!el) el = _panelEl.querySelector('#' + id); return el; } function setCourse(name) { _currentCourse = name; const el = _getEl('airare-course'); if (el) el.textContent = name || '--'; } function setProgress(text) { const el = _getEl('airare-progress'); if (el) el.textContent = text || '--'; if (text && text.includes('/')) { const parts = text.split('/'); const current = parseInt(parts[0]) || 0; const total = parseInt(parts[1]) || 1; const pct = Math.min(100, Math.round((current / total) * 100)); if (_quizProgressBar) { _quizProgressBar.style.display = 'block'; const root = _quizProgressBar.getRootNode(); const fill = root.querySelector('#airare-quiz-progress-fill') || _quizProgressBar.querySelector('#airare-quiz-progress-fill'); if (fill) fill.style.width = pct + '%'; } const hint = _getEl('airare-quiz-hint'); if (hint) { hint.style.display = 'block'; hint.textContent = `答题进度: ${current}/${total} (${pct}%)`; } _quizSolvedCount = current; _quizTotalCount = total; const cntEl = _getEl('airare-quiz-count'); if (cntEl) cntEl.textContent = current; } } /** 更新 AI 调用次数 */ function incrementAiCount() { _aiCallCount++; const el = _getEl('airare-ai-count'); if (el) el.textContent = _aiCallCount; } /** 更新任务点状态 */ function setTaskStatus(videoDone, quizDone) { const el = _getEl('airare-task-status'); if (el) { const v = videoDone ? '✓已看' : '○未看'; const q = quizDone ? '✓已做' : '○未做'; el.textContent = `视频${v} · 测验${q}`; } } function setDotState(active) { const el = _getEl('airare-dot'); if (!el) return; if (active) el.classList.remove('off'); else el.classList.add('off'); } let _initialized = false; // ========== 初始化 ========== function init() { if (_initialized) return; _initialized = true; // CSS 在 _buildPanelDOM 中直接注入 Shadow DOM,无需 GM_addStyle _buildPanelDOM(); _bindEvents(); Logger.success('控制面板已加载'); setDotState(true); } return { init, minimize, expand, close, setCourse, setProgress, setDotState, setTaskStatus, incrementAiCount, get isMinimized() { return _isMinimized; }, get aiCallCount() { return _aiCallCount; }, get quizSolvedCount() { return _quizSolvedCount; } }; })(); // ============================================================ // AntiCheat - 反作弊绕过引擎 // ============================================================ const AntiCheat = (function () { let _enabled = false; let _mouseTimer = null; let _cleanupFns = []; // ---------- 1. 页面可见性绕过 ---------- /** 覆盖 document.visibilityState → 始终 "visible" */ function _spoofVisibility() { const origDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'visibilityState'); if (!origDescriptor) return; Object.defineProperty(Document.prototype, 'visibilityState', { get() { return 'visible'; }, configurable: true }); // 同时覆盖 document.hidden const hiddenDesc = Object.getOwnPropertyDescriptor(Document.prototype, 'hidden'); if (hiddenDesc) { Object.defineProperty(Document.prototype, 'hidden', { get() { return false; }, configurable: true }); } } /** 拦截 visibilitychange 事件 */ function _blockVisibilityEvent() { function handler(e) { e.stopImmediatePropagation(); e.preventDefault(); } document.addEventListener('visibilitychange', handler, true); _cleanupFns.push(() => document.removeEventListener('visibilitychange', handler, true)); } // ---------- 2. 窗口失焦/页面隐藏拦截 ---------- function _blockBlurEvents() { // 拦截 blur 事件(捕获阶段) const blurHandler = (e) => { if (e.target === window) { e.stopImmediatePropagation(); e.preventDefault(); } }; window.addEventListener('blur', blurHandler, true); _cleanupFns.push(() => window.removeEventListener('blur', blurHandler, true)); // 拦截 pagehide const pagehideHandler = (e) => { e.stopImmediatePropagation(); e.preventDefault(); }; window.addEventListener('pagehide', pagehideHandler, true); _cleanupFns.push(() => window.removeEventListener('pagehide', pagehideHandler, true)); // 拦截 beforeunload(仅阻止学习通自己的检测) const unloadHandler = () => { // 静默处理,不阻止真正的页面关闭 }; window.addEventListener('beforeunload', unloadHandler); _cleanupFns.push(() => window.removeEventListener('beforeunload', unloadHandler)); } // ---------- 3. 视频/音频播放器绕过 ---------- /** * 覆盖 video.muted / audio.muted → 始终返回 true * 让平台认为播放器已静音(部分平台要求静音才能播放) */ function _spoofMediaMuted() { const origMutedDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'muted'); if (!origMutedDesc) return; Object.defineProperty(HTMLMediaElement.prototype, 'muted', { get() { return true; }, set(val) { // 静默接受,不修改实际 DOM 属性(避免平台检测到 override) // 保持原型链上的实际 muted 为 true }, configurable: true }); // 拦截 volumechange 事件,防止平台监听静音状态变化 document.addEventListener('volumechange', (e) => { e.stopImmediatePropagation(); }, true); } /** * 解除播放速率限制 * 超星学习通默认限制最大2倍速,我们绕过限制允许任意倍速 */ function _bypassPlaybackRateLimit() { const origRateDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); if (!origRateDesc) return; let _internalRate = 1; Object.defineProperty(HTMLMediaElement.prototype, 'playbackRate', { get() { if (this._aire_actualRate !== undefined) { return this._aire_actualRate; } return _internalRate; }, set(val) { // 学习通可能限制 rate 在 [0.5, 2.0] // 我们实际应用用户设置的倍速,但返回平台期望的值 this._aire_actualRate = val; // 调内部原生 setter 实现真正的倍速 if (origRateDesc.set) { origRateDesc.set.call(this, val); } }, configurable: true }); } /** * 禁用默认播放速率(defaultPlaybackRate) */ function _spoofDefaultPlaybackRate() { const origDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'defaultPlaybackRate'); if (!origDesc) return; Object.defineProperty(HTMLMediaElement.prototype, 'defaultPlaybackRate', { get() { return 1; }, set() {}, configurable: true }); } // ---------- 4. 弹窗劫持 ---------- function _hijackAlerts() { if (!unsafeWindow || typeof unsafeWindow === 'undefined') return; const win = unsafeWindow; // 劫持 alert → 静默关闭 win.alert = function () { if (_enabled) return undefined; // 降级到原始行为 return window.alert.apply(window, arguments); }; // 劫持 confirm → 自动返回 true win.confirm = function () { if (_enabled) return true; return window.confirm.apply(window, arguments); }; // 劫持 prompt → 返回空字符串(防止填null崩溃) win.prompt = function () { if (_enabled) return ''; return window.prompt.apply(window, arguments); }; console.log(`[${SCRIPT_NAME}] AntiCheat: alert/confirm/prompt 已劫持`); } // ---------- 5. 鼠标模拟防挂机 ---------- /** * 每30秒在页面随机位置触发一次微小鼠标移动 * 防止超星学习通的挂机检测(超过一定时间无操作会弹窗或暂停计时) */ function _simulateMouseMove() { function move() { if (!_enabled) return; const x = Math.random() * window.innerWidth; const y = Math.random() * window.innerHeight; try { const evt = new MouseEvent('mousemove', { view: window, bubbles: true, cancelable: true, clientX: x, clientY: y, movementX: Math.random() * 2 - 1, movementY: Math.random() * 2 - 1 }); document.dispatchEvent(evt); } catch (e) { // 静默失败 } } _mouseTimer = setInterval(move, 30000); } function _stopMouseSimulation() { if (_mouseTimer) { clearInterval(_mouseTimer); _mouseTimer = null; } } // ---------- 6. 额外防护 ---------- /** * 拦截页面的定时弹窗检测 * 超星学习通有时会通过 setInterval 检测是否有弹窗出现 */ function _blockPopupDetectors() { // 覆盖 window.open(防止脚本通过 window.open 检测弹窗拦截器) if (unsafeWindow && typeof unsafeWindow !== 'undefined') { const origOpen = unsafeWindow.open; unsafeWindow.open = function (url, target, features) { if (_enabled) { // 允许打开,但返回一个假的 window 引用防止检测 try { return origOpen.call(window, url, target, features); } catch (e) { return { closed: false, close() {} }; } } return origOpen.call(window, url, target, features); }; } } /** * 阻止超星学习通的暂停检测(当页面不可见时暂停播放器) */ function _preventAutoPause() { // 阻止一切非用户主动的暂停(鼠标离开播放器等) document.addEventListener('pause', (e) => { const target = e.target; if (target instanceof HTMLMediaElement && _enabled && !target.ended) { // 如果暂停是由鼠标离开或其他非用户操作触发,立即恢复 if (!target._aire_userPaused) { setTimeout(() => { try { if (target.paused && !target.ended) { target.play().catch(() => {}); } } catch (err) {} }, 50); } // 用完立即重置,下次还能自动恢复 target._aire_userPaused = false; } }, true); // 鼠标离开播放器 → 不标记为用户暂停 // 仅在用户主动点暂停按钮时标记(通过 click 事件区分) document.addEventListener('click', (e) => { // 如果点击的是播放/暂停按钮,标记为用户操作 const btn = e.target.closest('.vjs-play-control, .vjs-big-play-button, [title*="暂停"], [title*="播放"]'); if (btn) { // 找到最近的 video const vid = btn.closest('.video-js')?.querySelector('video'); if (vid) vid._aire_userPaused = false; // 不阻止,让正常切换 } }); } // ========== 公共接口 ========== function enable() { if (_enabled) return; _spoofVisibility(); _blockVisibilityEvent(); _blockBlurEvents(); _spoofMediaMuted(); _bypassPlaybackRateLimit(); _spoofDefaultPlaybackRate(); _hijackAlerts(); _blockPopupDetectors(); _preventAutoPause(); _simulateMouseMove(); _enabled = true; Logger.success('反作弊引擎已启动'); console.log(`[${SCRIPT_NAME}] AntiCheat: 所有绕过策略已就绪`); } function disable() { if (!_enabled) return; _stopMouseSimulation(); // 执行所有清理函数 _cleanupFns.forEach(fn => { try { fn(); } catch (e) {} }); _cleanupFns = []; _enabled = false; Logger.warn('反作弊引擎已停止'); } function isEnabled() { return _enabled; } // 监听配置变更(自动答题/自动刷课开关关联反作弊开关) EventBus.on(EventBus.CONFIG_CHANGED, ({ key, val }) => { if (key === 'autoPlay' || key === 'autoQuiz') { const autoPlay = ConfigStore.get('autoPlay'); const autoQuiz = ConfigStore.get('autoQuiz'); if ((autoPlay || autoQuiz) && !_enabled) { enable(); } else if (!autoPlay && !autoQuiz && _enabled) { disable(); } } }); return { enable, disable, isEnabled }; })(); // ============================================================ // CoursePlayer - 自动刷课引擎 // ============================================================ const CoursePlayer = (function () { let _active = false; let _observer = null; let _currentMedia = null; let _currentIframe = null; let _courseName = ''; let _progressCheckTimer = null; let _popupCloseTimer = null; // 当前章节信息 let _chapterInfo = { name: '', total: 0, current: 0 }; // ---------- 1. iframe 递归查找 ---------- /** * 递归搜索所有 iframe 中的媒体元素 * @param {Document} doc * @returns {Array<{el: HTMLMediaElement, win: Window, doc: Document, iframe: HTMLIFrameElement|null}>} */ function _findAllMedia(doc = document) { const results = []; // 当前文档中的媒体元素 doc.querySelectorAll('video, audio').forEach(el => { results.push({ el, win: doc.defaultView || window, doc, iframe: _currentIframe }); }); // 递归处理 iframe const iframes = doc.querySelectorAll('iframe'); iframes.forEach(iframe => { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (iframeDoc) { const subResults = _findAllMedia(iframeDoc); subResults.forEach(r => { r.iframe = iframe; results.push(r); }); } } catch (e) { // 跨域 iframe 跳过 } }); return results; } /** * 在指定文档中查找播放按钮 * 超星学习通的播放按钮可能是以下几种: * - .vjs-big-play-button (Video.js) * - .ans-attach-play-btn * - button[title*="播放"] * - .play-btn / .start-btn * @param {Document} doc * @returns {Element|null} */ function _findPlayButton(doc = document) { const selectors = [ '.vjs-big-play-button', '.ans-attach-play-btn', '[class*="play-btn"]', '[class*="play_btn"]', 'button[title*="播放"]', '.ans-cc-player-playbtn', '.video-play-btn', '.xv-player-btn', '[class*="xv-play"]', '.player-play', '[class*="start"]', '.ans-job-iconfont.icon-play' ]; for (const sel of selectors) { try { const btn = doc.querySelector(sel); if (btn) return btn; } catch (e) {} } return null; } // ---------- 2. 播放控制 ---------- /** * 自动点击播放按钮 */ function _clickPlay() { // 查找主文档中的播放按钮 let playBtn = _findPlayButton(document); if (playBtn) { _safeClick(playBtn); Logger.play('已点击播放按钮'); return true; } // 递归查找 iframe 中的播放按钮 const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (iframeDoc) { playBtn = _findPlayButton(iframeDoc); if (playBtn) { _safeClick(playBtn); _currentIframe = iframe; Logger.play('已点击 iframe 内播放按钮'); return true; } } } catch (e) {} } return false; } /** * 安全点击(模拟真实点击,绕过防自动化检测) */ function _safeClick(el) { if (!el) return; // 先触发鼠标事件序列 ['mousedown', 'mouseup', 'click'].forEach(eventType => { const evt = new MouseEvent(eventType, { view: window, bubbles: true, cancelable: true, clientX: el.getBoundingClientRect().left + 10, clientY: el.getBoundingClientRect().top + 10 }); el.dispatchEvent(evt); }); // 也尝试原生 click(兜底) try { el.click(); } catch (e) {} } /** * ============================================================ * 模拟播放加速系统 * * 学习通不允许拖动进度条,且限制 playbackRate。 * 正确做法:视频正常播放,但向服务器汇报的"观看时长" * 按倍速放大。例如 2x 倍速 = 每秒真实时间向服务器报 2 秒。 * * 加速效果体现在"任务进度"而非视频画面速度。 * ============================================================ */ /** * 检测当前视频任务点是否已完成 * 判断依据:aria-label 含"已完成"的 ans-job-icon */ function _isVideoTaskCompleted() { try { const doneIcons = document.querySelectorAll('.ans-job-icon[aria-label*="已完成"]'); if (doneIcons.length === 0) return false; // 检查当前激活的是否为视频标签 const activeTab = document.querySelector('#prev_tab .active, li.active[id^="dct"]'); return !!(activeTab && activeTab.id === 'dct1'); } catch (e) { return false; } } /** * 切换到章节测验后,多策略深度检测是否已做 * 策略:图标 → 批改 → iframe → 预填答案 → 重新作答按钮 → 兜底 */ function _afterSwitchToQuiz() { const check = (attempt) => { if (attempt > 6) return; const delay = 3000 + (attempt * 2000); // 3/5/7/9/11/13/15s setTimeout(() => { let found = false; // 1) 任务点图标(顶层文档) const topDoc = (window.top && window.top !== window) ? window.top.document : document; const doneIcons = topDoc.querySelectorAll('.ans-job-icon[aria-label*="已完成"]'); if (doneIcons.length >= 2) { found = true; Logger.play('📌 检测到2个已完成任务点'); } // 2) 当前任意文档的批改标记 if (!found) { const allDocs = [document]; try { document.querySelectorAll('iframe').forEach(f => { try { if (f.contentDocument) allDocs.push(f.contentDocument); } catch (e) {} }); } catch (e) {} for (const doc of allDocs) { const marks = doc.querySelectorAll('.marking_dui, .marking_cuo, .marking_half'); if (marks.length >= 1) { found = true; Logger.play('📌 检测到批改标记'); break; } } } // 3) 重新作答/再测按钮 if (!found) { const redoSelectors = '[onclick*="redo"], [onclick*="again"], .redo-btn'; if (document.querySelector(redoSelectors)) { found = true; } else { // 遍历 iframe 查找 document.querySelectorAll('iframe').forEach(f => { try { if (f.contentDocument && f.contentDocument.querySelector(redoSelectors)) found = true; } catch (e) {} }); } // 纯文本匹配"重新作答"/"再测一次" if (!found) { const btns = document.querySelectorAll('button, a, .jb_btn'); for (const b of btns) { if (/重新作答|再测一次|再做一次/.test(b.textContent)) { found = true; break; } } } if (found) Logger.play('📌 检测到重新作答按钮'); } // 4) 题目已有预填答案(li.check_answer, .cur) if (!found) { const preFilled = document.querySelectorAll('li.check_answer, li.cur, .num_option_dx.check_answer, .num_option_sx.check_answer'); if (preFilled.length >= 1) { found = true; Logger.play('📌 检测到预填答案'); } } // 5) 检测到测验已经显示成绩/分数 if (!found) { const scoreEl = document.querySelector('.score, .markScore, .grade, [class*="score"]'); if (scoreEl && scoreEl.textContent && /\d+/.test(scoreEl.textContent)) { found = true; Logger.play('📌 检测到测验成绩'); } } if (found) { _clickNextSectionBtn('📌 测验已完成,跳转下一节'); return; } // 继续轮询 if (attempt < 6) check(attempt + 1); }, delay); }; check(0); } function _clickNextSectionBtn(logMsg) { Logger.play(logMsg); const nextBtn = document.querySelector( '#prevNextFocusNext, .next.fr, .prev_next.next, ' + '.jb_btn.next, div[onclick*="PCount.next"]:not([class*="preChapter"])' ); if (nextBtn && nextBtn.offsetParent !== null) { nextBtn.click(); Logger.play('🤖 已跳转下一节'); } } /** * 周期性检查任务点状态,智能跳过已完成的内容 */ let _quizCheckActive = false; function _checkAndSkipTaskPoints() { try { const topDoc = (window.top && window.top !== window) ? window.top.document : document; const doneIcons = topDoc.querySelectorAll('.ans-job-icon[aria-label*="已完成"]'); const doneCount = doneIcons.length; // 如果顶层文档没有任务点图标,尝试用其他方式检测(点任务点链接的 class) let actualDone = doneCount; if (actualDone === 0) { // 备用检测:任务点可能用 class 标记完成状态 const altIcons = topDoc.querySelectorAll('.ans-job-icon.ans-job-finished, .ans-job-icon.finished, [class*="job-icon"][class*="finish"], .ans-job-icon .ans-job-finished'); actualDone = altIcons.length; } PanelMgr.setTaskStatus(actualDone >= 1, actualDone >= 2); if (actualDone >= 2) { _clickNextSectionBtn('📌 视频+测验任务点均已完成,跳转下一节'); _quizCheckActive = false; return; } if (actualDone === 1) { const activeVideo = topDoc.querySelector('#dct1.active, li.active[id^="dct"][title*="视频"]'); const activeQuiz = topDoc.querySelector('#dct2.active, li.active[id^="dct"][title*="测验"]'); if (activeVideo) { Logger.play('📌 视频任务点已完成,切换到章节测验'); const quizTab = topDoc.getElementById('dct2'); if (quizTab && quizTab.offsetParent !== null) { quizTab.click(); Logger.play('📝 已切换到章节测验'); _quizCheckActive = true; _afterSwitchToQuiz(); } } else if (activeQuiz && !_quizCheckActive) { Logger.play('📌 测验标签激活,启动深度检测'); _quizCheckActive = true; _afterSwitchToQuiz(); } } // 兜底:即使图标检测不到,也检查 iframe 内的测验是否已批改 if (actualDone === 0 || actualDone === 1) { let quizDone = false; topDoc.querySelectorAll('iframe').forEach(f => { if (quizDone) return; try { const doc = f.contentDocument; if (!doc) return; if (doc.querySelectorAll('.marking_dui, .marking_cuo').length >= 1) { quizDone = true; } if (doc.querySelector('[onclick*="redo"], .redo-btn')) quizDone = true; } catch (e) {} }); if (quizDone && !_quizCheckActive) { Logger.play('📌 iframe 内检测到已批改测验,跳转下一节'); _clickNextSectionBtn('📌 iframe 内检测到已批改测验,跳转下一节'); _quizCheckActive = false; } } } catch (e) {} } /** * 对指定的媒体启动模拟计时 * vid 用 1x 正常播,我们的 timer 按 speed 倍率累计播放时间 * 当累计时间 >= duration 时任务完成 */ function _startSimulateTimer(el, duration) { if (!el || !duration) return; if (el._aire_simulating) return; // 智能检测:视频任务点已完成 → 直接跳到章节测验 const videoDone = _isVideoTaskCompleted(); if (videoDone) { Logger.play('📌 视频任务点已完成,跳过视频直接去章节测验'); const quizTab = document.getElementById('dct2'); if (quizTab && quizTab.offsetParent !== null) { quizTab.click(); Logger.play('📝 已切换到章节测验'); _afterSwitchToQuiz(); } return; } el._aire_simulating = true; let simulatedTime = el.currentTime || 0; // 从当前位置开始 let lastReal = performance.now(); let timerId = null; function tick() { if (!_active) { // 已停止则退出 el._aire_simulating = false; if (timerId) { clearInterval(timerId); timerId = null; } return; } // 视频暂停时尝试恢复播放 if (el.paused && !el.ended) { try { el.play().catch(() => {}); } catch (e) {} } if (el.ended) { return; } // 真正结束才跳过 const now = performance.now(); const realDelta = (now - lastReal) / 1000; lastReal = now; const speed = ConfigStore.get('speed'); // 按倍速放大累计时间 simulatedTime += realDelta * speed; // 更新面板进度 const pct = Math.min(Math.floor((simulatedTime / duration) * 100), 100); PanelMgr.setProgress(`${pct}%`); // 每 10% 打一次日志 const pctMark = Math.floor(pct / 10) * 10; if (el._aire_lastPct !== pctMark && pctMark % 10 === 0 && pctMark > 0) { el._aire_lastPct = pctMark; Logger.play(`模拟进度: ${pct}%`); } // 完成时触发:优先切到章节测验,没有则跳章节 if (simulatedTime >= duration - 1) { Logger.play('模拟播放完成'); PanelMgr.setProgress('100%'); el._aire_simulating = false; if (timerId) { clearInterval(timerId); timerId = null; } EventBus.emit(EventBus.PLAYBACK_COMPLETE, { el, duration }); // 优先检查是否有"章节测验"标签 const quizTab = document.getElementById('dct2'); if (quizTab && quizTab.offsetParent !== null) { quizTab.click(); Logger.play('📝 已切换到章节测验'); _afterSwitchToQuiz(); } else { _goNextChapter(); } } } timerId = setInterval(tick, 1000); // 存储清理 el._aire_speedCleanup = () => { el._aire_simulating = false; if (timerId) { clearInterval(timerId); timerId = null; } delete el._aire_speedCleanup; delete el._aire_lastPct; }; } /** * 扫描所有 media 启动模拟计时 */ function _scanAndSimulate() { const mediaList = _findAllMedia(); mediaList.forEach(({ el }) => { if (el.duration && isFinite(el.duration) && el.duration > 1) { _startSimulateTimer(el, el.duration); } }); } /** 3 秒轮询扫描新的 video */ let _simulateScanTimer = null; function _startSimulateScan() { if (_simulateScanTimer) return; _simulateScanTimer = setInterval(() => { if (!_active) return; _scanAndSimulate(); }, 3000); } function _stopSimulateScan() { if (_simulateScanTimer) { clearInterval(_simulateScanTimer); _simulateScanTimer = null; } } function _stopAllSimulate() { const mediaList = _findAllMedia(); mediaList.forEach(({ el }) => { if (el._aire_speedCleanup) el._aire_speedCleanup(); }); } // ---------- 3. 进度监控 ---------- /** * 监控视频播放进度 */ function _monitorProgress(media) { if (!media || _currentMedia === media) return; _currentMedia = media; // 监听进度更新 const onTimeUpdate = throttle(() => { if (!media.duration || !isFinite(media.duration)) return; const pct = ((media.currentTime / media.duration) * 100).toFixed(1); PanelMgr.setProgress(`${pct}%`); // 每 10% 进度打一次日志 const pctNum = Math.floor(parseFloat(pct) / 10) * 10; if (media._aire_lastPct !== pctNum && pctNum % 10 === 0) { media._aire_lastPct = pctNum; Logger.play(`播放进度: ${pct}%`); } }, 2000); // 监听播放结束 → 先切到章节测验,测验完成后自动跳下一章 const onEnded = () => { Logger.play('当前视频播放完毕'); EventBus.emit(EventBus.PLAYBACK_COMPLETE, { media, courseName: _courseName }); // 优先检查是否有"章节测验"标签,有则切换过去 const quizTab = document.getElementById('dct2'); if (quizTab && quizTab.offsetParent !== null) { quizTab.click(); Logger.play('📝 已切换到章节测验'); _afterSwitchToQuiz(); } else { _goNextChapter(); } }; // 监听播放错误 const onError = () => { Logger.error('视频播放出错,尝试重新加载'); EventBus.emit(EventBus.PLAYBACK_ERROR, { media, error: media.error }); setTimeout(() => { try { media.play().catch(() => {}); } catch (e) {} }, 3000); }; media.addEventListener('timeupdate', onTimeUpdate); media.addEventListener('ended', onEnded); media.addEventListener('error', onError); // 存储清理函数 media._aire_cleanup = () => { media.removeEventListener('timeupdate', onTimeUpdate); media.removeEventListener('ended', onEnded); media.removeEventListener('error', onError); delete media._aire_cleanup; delete media._aire_lastPct; }; Logger.play('开始监控播放进度'); } // ---------- 4. 章节跳转 ---------- /** * 跳转到下一个章节 */ function _goNextChapter() { // 参考脚本选择器 + 通用兜底 const nextSelectors = [ '#prevNextFocusNext', '.jb_btn.jb_btn_92.fr.fs14.nextChapter', '#nextBtn', '.nextChapter', '.next_chapter', '.nav_next', '.catalog_next', '.pos_next', '.ans-next', 'a[title*="下一"]', 'button[title*="下一"]' ]; for (const sel of nextSelectors) { try { const btn = document.querySelector(sel); if (btn && !btn.disabled) { _safeClick(btn); Logger.success('已跳转到下一章节'); // 重置当前媒体引用,让 observer 重新发现 _resetCurrentMedia(); return; } } catch (e) {} } // 如果在 iframe 中,也搜索 iframe 内的下一章按钮 const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (!iframeDoc) continue; for (const sel of nextSelectors) { const btn = iframeDoc.querySelector(sel); if (btn && !btn.disabled) { _safeClick(btn); Logger.success('已跳转到下一章节(iframe)'); _resetCurrentMedia(); return; } } } catch (e) {} } Logger.warn('未找到下一章节按钮,可能已是最后一章'); } function _resetCurrentMedia() { if (_currentMedia) { if (_currentMedia._aire_cleanup) _currentMedia._aire_cleanup(); if (_currentMedia._aire_speedCleanup) _currentMedia._aire_speedCleanup(); } _currentMedia = null; } // ---------- 5. 弹窗处理 ---------- /** * 自动关闭各种提示弹窗 */ // ---------- 5b. 视频内答题自动处理 ---------- /** * 检测视频播放中弹出的题目(.ans-videoquiz-opt) * 用 AI 智能答题,防止卡住视频播放 */ let _videoQuizTimer = null; let _videoQuizSolving = false; // 防重入 function _startVideoQuizHandler() { if (_videoQuizTimer) return; let _debugCount = 0; _videoQuizTimer = setInterval(() => { if (!_active) return; try { _debugCount++; // 主文档 _handleVideoQuizPopup(document); // iframe 内(含嵌套) const scanIframes = (root) => { root.querySelectorAll('iframe').forEach(iframe => { try { const doc = iframe.contentDocument || iframe.contentWindow?.document; if (doc) { _handleVideoQuizPopup(doc); scanIframes(doc); // 嵌套 iframe } } catch (e) {} }); }; scanIframes(document); // 每 10 次输出一次扫描状态 if (_debugCount % 10 === 0) { const quizInMain = document.querySelector('#videoquiz-submit'); const iframes = document.querySelectorAll('iframe'); Logger.play(`📺 扫描#${_debugCount}: 主文档quiz=${!!quizInMain} iframe=${iframes.length}个`); } } catch (e) {} }, 1000); } async function _handleVideoQuizPopup(doc) { if (_videoQuizSolving) return; const submitBtn = doc.querySelector('#videoquiz-submit'); if (!submitBtn) return; // 用 getComputedStyle 检查真正可见性(offsetParent 对绝对定位可能为 null) const win = doc.defaultView || doc.ownerDocument?.defaultView; const btnStyle = win ? win.getComputedStyle(submitBtn) : null; if (btnStyle && btnStyle.display === 'none') return; // 如果正在提交中(#videoquiz-submitting 可见),跳过 const submitting = doc.querySelector('#videoquiz-submitting'); if (submitting) { const subStyle = win ? win.getComputedStyle(submitting) : null; if (subStyle && subStyle.display !== 'none') return; } const opts = doc.querySelectorAll('.ans-videoquiz-opt label'); if (opts.length === 0) return; // 只有检测到题目文本才确认是弹窗 const titleEl = doc.querySelector('.tkItem_title'); if (!titleEl || !titleEl.textContent.trim()) return; _videoQuizSolving = true; try { // 提取题目和选项 const titleEl = doc.querySelector('.tkItem_title'); const typeEl = doc.querySelector('.tkTopic_title'); const question = titleEl ? titleEl.textContent.trim() : ''; const typeText = typeEl ? typeEl.textContent.trim() : '单选题'; let type = 'single'; if (typeText.includes('判断')) type = 'judge'; else if (typeText.includes('多选')) type = 'multi'; else if (typeText.includes('填空')) type = 'fill'; const optTexts = []; opts.forEach((opt, i) => { const letter = String.fromCharCode(65 + i); const text = opt.textContent.trim().replace(/^[A-D][、,\s]*/, ''); optTexts.push(text); }); Logger.play(`📺 视频答题: ${question.substring(0, 30)}... [${type}]`); // 用 AI 获取答案 let answer = null, answers = null; if (typeof AISolver !== 'undefined' && AISolver.isConfigured()) { const aiResult = await AISolver.askAI(question, type, optTexts); if (aiResult.success) { answer = aiResult.answer; answers = aiResult.answers; Logger.play(`🤖 AI答案: ${answer}`); } } // 如果没有 AI 或 AI 失败,用兜底 if (!answer && (!answers || answers.length === 0)) { if (type === 'judge') { answer = '对'; answers = ['对']; } else if (type === 'multi') { const letters = opts.map((_, i) => String.fromCharCode(65 + i)); answer = letters.join(''); answers = letters; } else { answer = String.fromCharCode(65 + Math.min(2, opts.length - 1)); answers = [answer]; } Logger.play(`📺 兜底答案: ${answer}`); } // 点击选项 if (type === 'judge') { // 判断题:对=索引0,错=索引1 const idx = /^(对|正确|√|是|✓|T|true|1)$/i.test(answer || '') ? 0 : 1; if (idx < opts.length) opts[idx].click(); } else { const letter = answer ? answer.trim().toUpperCase().charAt(0) : ''; if (letter >= 'A' && letter <= 'Z') { const idx = letter.charCodeAt(0) - 65; if (idx >= 0 && idx < opts.length) opts[idx].click(); } } if (type === 'multi' && answers) { for (const a of answers) { const aIdx = String(a).toUpperCase().charCodeAt(0) - 65; if (aIdx >= 0 && aIdx < opts.length) { opts[aIdx].click(); } } } // 提交 submitBtn.click(); Logger.play('📺 视频答题已提交'); // 等结果出来后点"继续" setTimeout(() => { const continueBtn = doc.querySelector('#videoquiz-continue'); if (continueBtn && continueBtn.offsetParent !== null) { continueBtn.click(); Logger.play('📺 视频答题→继续播放'); } }, 1500); } catch (e) { Logger.error('视频答题处理失败: ' + e.message); // 出错时随机选一个应急 if (opts.length > 0) { const pick = opts[Math.floor(Math.random() * opts.length)]; pick?.click(); submitBtn.click(); } } _videoQuizSolving = false; } function _stopVideoQuizHandler() { if (_videoQuizTimer) { clearInterval(_videoQuizTimer); _videoQuizTimer = null; } } function _startPopupCloser() { // 每 3 秒检查一次弹窗 _popupCloseTimer = setInterval(() => { if (!_active) return; _closePopups(); }, 3000); } function _closePopups() { // 常见的弹窗/确认框选择器 const popupSelectors = [ // 未完成/继续观看弹窗 '.dialog-btn.continue', '.dialog-btn.ok', '.ans-dialog .btn-primary', '.el-dialog__footer .el-button--primary', // 章节完成弹窗 '.complete-btn', '.finish-btn', // 确认弹窗 '.confirm .btn-ok', '.message-box .btn-confirm', // 通用弹窗按钮 '.popup .confirm', '.modal .btn-primary', '.layui-layer-btn0', // 超星确认 '.ans-attach-online .confirm', '.dialog-footer .confirm-btn', '.submit-btn', // 验证弹窗的关闭 '.close-pop', '.dialog-close' ]; let closed = false; for (const sel of popupSelectors) { try { const btns = document.querySelectorAll(sel); for (const btn of btns) { if (btn.offsetParent !== null) { // 可见的 _safeClick(btn); closed = true; } } } catch (e) {} } if (closed) { Logger.info('已自动关闭弹窗'); } } function _stopPopupCloser() { if (_popupCloseTimer) { clearInterval(_popupCloseTimer); _popupCloseTimer = null; } } // ---------- 6. DOM 监听 ---------- /** * 启动 MutationObserver 监听媒体元素出现 */ function _startObserver() { if (_observer) _observer.disconnect(); _observer = new MutationObserver(debounce((mutations) => { if (!_active) return; const config = ConfigStore.getAll(); if (!config.autoPlay) return; // 检查是否有新的媒体元素出现 const hasNewMedia = mutations.some(m => Array.from(m.addedNodes).some(node => { if (node.nodeType !== 1) return false; return node.tagName === 'VIDEO' || node.tagName === 'AUDIO' || node.querySelector && (node.querySelector('video') || node.querySelector('audio')); }) ); if (hasNewMedia || _currentMedia === null) { _tryPlayMedia(); } }, 500)); _observer.observe(document.body, { childList: true, subtree: true, attributes: false }); Logger.info('DOM监听已启动,等待课程内容加载...'); } /** * 尝试找到并播放媒体 */ function _tryPlayMedia() { // 1. 先尝试点击播放按钮 _clickPlay(); // 2. 找到所有媒体元素并开始播放 const mediaList = _findAllMedia(); for (const { el } of mediaList) { // 尝试播放 + 启动模拟计时 if (el.paused || el.ended) { el.play().then(() => { Logger.play('媒体已开始播放'); _monitorProgress(el); PanelMgr.setDotState(true); }).catch(err => { // 自动播放可能被浏览器阻止,尝试静音播放 try { el.muted = true; el.play().then(() => { Logger.play('已静音启动播放'); _monitorProgress(el); }).catch(() => {}); } catch (e) {} }); } else { // 已经在播放 _monitorProgress(el); } } // 3. 尝试获取课程名称 _detectCourseName(); } /** * 检测当前课程名称 */ function _detectCourseName() { const nameSelectors = [ '.prev_title', // 章节详情页标题 '.catalog_title', // 章节标题 span '.catalog_tit', // 变体 '.chapter_tit', // 当前章节 '.currents .catalog_tit', // 高亮当前章节 '.ans-bread-crumb span:last-child', // 面包屑最后一级 '.position a.active', '.course_name', '.coursename' ]; for (const sel of nameSelectors) { try { const el = document.querySelector(sel); if (!el) continue; const text = el.textContent.trim(); if (text && text.length > 1 && !/^[\d\s\.\-\—]+$/.test(text)) { if (text !== _courseName) { _courseName = text; PanelMgr.setCourse(text); Logger.info(`当前课程: ${text}`); } return; } } catch (e) {} } } // ---------- 7. 文档类型章节处理 ---------- /** * 处理文档/PPT类章节(非视频) * 超星学习通有些章节是 PDF/PPT 文档,需要模拟阅读行为 */ function _handleDocumentChapter() { // 检测是否为文档类型页面 const docSelectors = [ '.ans-attach-online', // PDF在线阅读器 '.pdf-wrapper', '.ppt-wrapper', '.ans-doc-viewer', '.ans-reader', '.iframe[src*="pdf"]', 'iframe[src*="ppt"]' ]; let isDoc = false; for (const sel of docSelectors) { if (document.querySelector(sel)) { isDoc = true; break; } } if (!isDoc) return; Logger.play('检测到文档类型章节,开始模拟阅读'); // 模拟滚动阅读行为 let scrollTimer = setInterval(() => { if (!_active) { clearInterval(scrollTimer); return; } window.scrollBy(0, 200); if (window.innerHeight + window.scrollY >= document.body.scrollHeight - 50) { // 滚到底部 = 读完 clearInterval(scrollTimer); Logger.success('文档阅读完成(已滚动到底部)'); setTimeout(() => _goNextChapter(), 2000); } }, 3000); // 存储清理函数 _currentMedia = { _aire_cleanup: () => { clearInterval(scrollTimer); } }; } // ---------- 公共接口 ---------- function start() { if (_active) return; _active = true; _startObserver(); _startPopupCloser(); _startVideoQuizHandler(); // 模拟进度已禁用,使用原倍速播放 // 立即检查一次 setTimeout(() => { _tryPlayMedia(); _handleDocumentChapter(); _detectCourseName(); _checkAndSkipTaskPoints(); }, 1500); // 每 5s 检查任务点:视频做过 → 切测验;测验也做过 → 下一节 setInterval(() => { _checkAndSkipTaskPoints(); }, 5000); // 每 2s 恢复被暂停的视频(鼠标离开播放器触发) setInterval(() => { const mediaList = _findAllMedia(); for (const { el } of mediaList) { if (el.paused && !el.ended) { try { el.play().catch(() => {}); } catch (e) {} } } }, 2000); Logger.success('自动刷课已启动'); EventBus.emit(EventBus.PLAYBACK_START, {}); } function stop() { if (!_active) return; _active = false; if (_observer) { _observer.disconnect(); _observer = null; } _resetCurrentMedia(); _stopPopupCloser(); _stopVideoQuizHandler(); _stopSimulateScan(); _stopAllSimulate(); PanelMgr.setProgress('--'); PanelMgr.setDotState(false); Logger.warn('自动刷课已停止'); } function isActive() { return _active; } // 监听配置变更 EventBus.on(EventBus.CONFIG_CHANGED, ({ key, val }) => { if (key === 'autoPlay') { if (val && !_active) { start(); } else if (!val && _active) { stop(); } } if (key === 'speed' && _active) { // 倍速变了不需要重启循环,rAF 每帧自动读取最新 ConfigStore.get('speed') Logger.info(`倍速已切换至 ${val}x`); } }); // 监听页面变化(SPA导航) EventBus.on(EventBus.PAGE_CHANGED, () => { if (_active) { _resetCurrentMedia(); setTimeout(() => { _tryPlayMedia(); _handleDocumentChapter(); _detectCourseName(); }, 2000); } }); return { start, stop, isActive }; })(); // ============================================================ // AnswerSearcher - 题库搜索引擎 // ============================================================ const AnswerSearcher = (function () { // ---------- 公开题库 API 定义 ---------- const API_LIST = [ { name: 'forestpolice', url: (q) => `https://api.forestpolice.top/api/query?q=${encodeURIComponent(q)}`, method: 'GET', headers: {}, body: null, parser(r) { const json = safeJSONParse(r.responseText, {}); return json.answer || json.data?.answer || json.data || null; } }, { name: 'lyck6', url: (q) => `https://lyck6.cn/api/v1/query`, method: 'POST', headers: { 'Content-Type': 'application/json' }, body: (q) => JSON.stringify({ question: q }), parser(r) { const json = safeJSONParse(r.responseText, {}); return json.answer || json.data?.answer || json.data || null; } }, { name: 'cxmooc-tool', url: (q) => `https://cxmooc-tools.avosapps.us/api/search?question=${encodeURIComponent(q)}`, method: 'GET', headers: {}, body: null, parser(r) { const json = safeJSONParse(r.responseText, {}); return json.answer || json.data || null; } }, { name: 'tikuhai', url: (q) => `https://api.tikuhai.com/search?q=${encodeURIComponent(q)}`, method: 'GET', headers: {}, body: null, parser(r) { const json = safeJSONParse(r.responseText, {}); if (Array.isArray(json.data) && json.data.length > 0) { return json.data[0].answer || json.data[0]; } return null; } }, { name: 'dayi', url: (q) => `https://api.dayi.im/query?q=${encodeURIComponent(q)}`, method: 'GET', headers: {}, body: null, parser(r) { const json = safeJSONParse(r.responseText, {}); return json.answer || json.data || null; } } ]; /** 单次请求超时 ms */ const REQUEST_TIMEOUT = 8000; // ---------- GM_xmlhttpRequest 封装 ---------- /** * 使用 GM_xmlhttpRequest 发起跨域请求 * @param {object} apiDef * @param {string} question * @returns {Promise<{answer: string|null, source: string}>} */ function _gmRequest(apiDef, question) { return new Promise((resolve) => { const url = typeof apiDef.url === 'function' ? apiDef.url(question) : apiDef.url; const details = { method: apiDef.method, url: url, headers: apiDef.headers || {}, timeout: REQUEST_TIMEOUT, onload: (r) => { if (r.status >= 200 && r.status < 300) { try { const answer = apiDef.parser(r); if (answer !== null && answer !== undefined) { resolve({ answer, source: apiDef.name }); return; } } catch (e) {} } resolve({ answer: null, source: apiDef.name }); }, onerror: () => resolve({ answer: null, source: apiDef.name }), ontimeout: () => resolve({ answer: null, source: apiDef.name }) }; if (apiDef.body) { details.data = typeof apiDef.body === 'function' ? apiDef.body(question) : apiDef.body; } try { GM_xmlhttpRequest(details); } catch (e) { resolve({ answer: null, source: apiDef.name }); } }); } // ---------- 文本处理 ---------- /** * 清洗题目文本 * - 去除多余空白 * - 去除题号前缀 (1. / (1) / 1、等) * - 统一全角/半角标点 */ function _cleanQuestion(text) { if (!text) return ''; let cleaned = text .replace(/[\s\u3000]+/g, ' ') .replace(/^[\(\(]?\d+[\)\)\.\、\s]+/, '') .trim(); // 如果太短(< 5 字符),保留原文 if (cleaned.length < 5 && text.length > 5) { cleaned = text.trim(); } return cleaned; } /** * 模糊匹配函数(用于填空题) * @param {string} a * @param {string} b * @returns {boolean} */ function _fuzzyMatch(a, b) { if (!a || !b) return false; const normalize = (s) => String(s) .replace(/[\s\u3000,,。;;::!!??"""''\(\)()\[\]【】\-—]+/g, '') .toLowerCase() .trim(); const na = normalize(a); const nb = normalize(b); if (na === nb) return true; // 包含关系 if (na.includes(nb) || nb.includes(na)) return true; // 编辑距离(简单版:公共子串比例) const commonChars = [...na].filter(c => nb.includes(c)).length; const ratio = commonChars / Math.max(na.length, nb.length); return ratio >= 0.75; } // ---------- 答案聚合 ---------- /** * 对答案列表去重、排序、统计置信度 * @param {Array<{answer: string, source: string}>} results * @param {string} type - 'single'|'multi'|'judge'|'fill' * @param {string[]} [options] - 题目选项列表 * @returns {{answer: string|null, confidence: number, detail: string}} */ function _aggregateResults(results, type, options = []) { const valid = results.filter(r => r.answer !== null); if (valid.length === 0) { return { answer: null, confidence: 0, detail: '所有题库无匹配结果' }; } // 统计每个答案出现次数 const counter = new Map(); for (const r of valid) { let ans = String(r.answer).trim(); // 规范化答案 ans = _normalizeAnswer(ans, type); const key = ans.toUpperCase(); if (!counter.has(key)) { counter.set(key, { answer: ans, count: 0, sources: [] }); } const entry = counter.get(key); entry.count++; entry.sources.push(r.source); } // 按出现次数降序排序 const sorted = [...counter.values()].sort((a, b) => b.count - a.count); const best = sorted[0]; const totalSources = sorted.reduce((sum, s) => sum + s.count, 0); const confidence = best.count / totalSources; // 根据题型做校验 let answer = _validateByType(best.answer, sorted, type, options); return { answer, confidence: Math.min(confidence, 1.0), detail: `${best.count}/${totalSources} 来源一致 (${best.sources.join(',')})` }; } /** * 规范化答案文本 */ function _normalizeAnswer(raw, type) { let ans = raw.replace(/^\s*答[::案]?\s*/i, '').trim(); // 判断题 → 对/错 if (type === 'judge') { if (/^(正确|对|√|是|✓|T|true|A)$/i.test(ans)) return '对'; if (/^(错误|错|×|否|✗|F|false|B)$/i.test(ans)) return '错'; // 兼容 1/0 if (ans === '1') return '对'; if (ans === '0') return '错'; } return ans; } /** * 按题型校验答案 */ function _validateByType(bestAnswer, allResults, type, options) { if (!bestAnswer) return null; switch (type) { case 'single': { // 单选题:答案应该是单个字母 A/B/C/D const match = bestAnswer.match(/^[A-D]$/i); if (match) return match[0].toUpperCase(); // 也可能是文字匹配选项 if (options.length > 0) { const matched = _matchToOption(bestAnswer, options); if (matched) return matched; } return bestAnswer; } case 'multi': { // 多选题:答案可能是 "AB" / "A,B" / ["A","B"] 等 const letters = bestAnswer.match(/[A-D]/gi); if (letters && letters.length > 0) { return [...new Set(letters.map(l => l.toUpperCase()))].sort().join(''); } if (options.length > 0) { const matched = _matchToOption(bestAnswer, options); if (matched) return matched; } return bestAnswer; } case 'judge': { // 判断题 if (/对|正确|√|是|✓|T|true|1/i.test(bestAnswer)) return '对'; if (/错|错误|×|否|✗|F|false|0/i.test(bestAnswer)) return '错'; return bestAnswer; } case 'fill': { // 填空题:返回原始文本(可能在多个来源间做模糊择优) if (allResults.length >= 2) { // 取所有结果中出现最频繁的答案 const counter = new Map(); for (const r of allResults) { const key = String(r.answer).trim(); counter.set(key, (counter.get(key) || 0) + 1); } let best = bestAnswer; let bestCount = 0; for (const [k, c] of counter) { if (c > bestCount) { best = k; bestCount = c; } } return best; } return bestAnswer; } default: return bestAnswer; } } /** * 将答案文本匹配到选项字母 * @param {string} answerText * @param {string[]} optionTexts * @returns {string|null} 选项字母 */ function _matchToOption(answerText, optionTexts) { if (!optionTexts || optionTexts.length === 0) return null; for (let i = 0; i < optionTexts.length; i++) { const opt = optionTexts[i]; const letter = String.fromCharCode(65 + i); // A, B, C, D... // 直接包含匹配 if (opt.includes(answerText) || answerText.includes(opt)) { return letter; } // 模糊匹配 if (_fuzzyMatch(opt, answerText)) { return letter; } } return null; } // ---------- 公开接口 ---------- /** * 搜索题目答案 * @param {string} question 题目文本 * @param {string} type 题型: 'single'|'multi'|'judge'|'fill' * @param {string[]} [options=[]] 选项文本列表 * @returns {Promise<{answer: string|null, confidence: number, detail: string, allResults: Array}>} */ async function search(question, type = 'single', options = []) { const cleaned = _cleanQuestion(question); if (!cleaned || cleaned.length < 3) { return { answer: null, confidence: 0, detail: '题目文本过短,无法搜索', allResults: [] }; } Logger.search(`搜索: ${cleaned.substring(0, 40)}...`); // 并发搜索所有 API const startTime = Date.now(); const promises = API_LIST.map(api => _gmRequest(api, cleaned)); // 超时总控(15 秒) const timeoutPromise = new Promise((resolve) => { setTimeout(() => { resolve(null); }, 15000); }); const results = await Promise.race([ Promise.all(promises).then(all => { const elapsed = Date.now() - startTime; Logger.search(`题库搜索完成 (${elapsed}ms, ${all.filter(r => r.answer).length}/${all.length} 命中)`); return all; }), timeoutPromise.then(() => { Logger.warn('题库搜索超时(15s),使用已有结果'); return []; }) ]); const validResults = Array.isArray(results) ? results : []; // 聚合结果 const aggregated = _aggregateResults(validResults, type, options); if (aggregated.answer) { Logger.search(`答案: ${aggregated.answer} (置信度: ${(aggregated.confidence * 100).toFixed(0)}%)`); } else { Logger.search('题库未找到答案'); } return { ...aggregated, allResults: validResults }; } /** * 填空题专用搜索(加强模糊匹配) */ async function searchFill(question, hints = []) { // 对填空题做特殊处理:如果题目包含多个空,可拆分搜索 const result = await search(question, 'fill', []); if (result.answer || !hints.length) return result; // 如果有提示词(如"第1空"),逐一搜索 const fillAnswers = []; for (const hint of hints) { const subResult = await search(`${question} ${hint}`, 'fill', []); if (subResult.answer) { fillAnswers.push(subResult.answer); } else { fillAnswers.push('?'); } } if (fillAnswers.some(a => a !== '?')) { result.answer = fillAnswers.join('; '); result.detail = '拆分搜索完成'; } return result; } return { search, searchFill }; })(); // ============================================================ // AISolver - AI 大模型答题引擎 // ============================================================ const AISolver = (function () { const MAX_RETRIES = 1; const TIMEOUT_MS = 60000; // ---------- Prompt 模板 ---------- /** 系统提示词 */ const SYSTEM_PROMPT = `你是超星学习通专业答题助手。用户会给你一道题目、选项列表和题型。 你需要仔细分析题目,选出正确答案,并以严格的JSON格式返回答案。 ⚠️ 重要规则(必须严格遵守): 1. 单选题(single):返回 {"answer": "A"} (单个选项字母,用 answer 字段) 2. 多选题(multi):返回 {"answers": ["A","C","D"]} (必须用 answers 数组字段,字母按顺序排列) 3. 判断题(judge):返回 {"answer": "对"} 或 {"answer": "错"} 4. 填空题(fill):返回 {"answers": ["答案1","答案2"]} (按空位顺序,用 answers 数组字段) 5. 简答题(short):返回 {"answers": ["答案内容"]} (用 answers 数组字段) 格式要求: - 必须在回复开头直接输出JSON,不要有任何其他文字 - 不要在JSON前后添加解释、思考过程或markdown代码块 - 必须使用双引号,不要使用单引号 - JSON必须是合法可解析的格式 - 多选题绝不能用 {"answer": "ABC"} 这种字符串拼接,必须用 {"answers": ["A","B","C"]} - 如果完全不确定答案,返回 {"answer": null}`; /** * 构建用户提示词 * @param {string} question 题目文本 * @param {string[]} options 选项列表 * @param {string} type 题型 * @returns {string} */ function _buildUserPrompt(question, options, type) { let prompt = '题目:' + question + '\n'; if (options && options.length > 0) { const letters = options.map((opt, i) => { return String.fromCharCode(65 + i) + '. ' + opt; }); prompt += '选项:\n' + letters.join('\n') + '\n'; } prompt += '题型:'; switch (type) { case 'single': prompt += '单选题(选择一个正确答案,用 {"answer": "字母"} 格式)'; break; case 'multi': prompt += '多选题(选择所有正确答案,必须用 {"answers": ["字母1","字母2"]} 数组格式,不要用字符串拼接!)'; break; case 'judge': prompt += '判断题(回答"对"或"错",用 {"answer": "对/错"} 格式)'; break; case 'fill': prompt += '填空题(按空位顺序填入答案,用 {"answers": ["答案1","答案2"]} 数组格式)'; break; case 'short': prompt += '简答题/论述题(回答答案内容,用 {"answers": ["答案内容"]} 数组格式)'; break; default: prompt += type + '(用 {"answer": "..."} 或 {"answers": ["..."]} 格式)'; } prompt += '\n\n请直接输出你的答案(纯JSON,不要添加任何其他文字):'; return prompt; } // ---------- GM_xmlhttpRequest 封装 ---------- /** * 发起一次 AI API 请求 * @param {string} question * @param {string[]} options * @param {string} type * @param {boolean} [force=false] 强制回答模式,绝不允许返回 null * @returns {Promise<{answer: string|null, answers: string[]|null, raw: string}>} */ function _requestAI(question, options, type, force = false) { return new Promise((resolve, reject) => { const apiKey = ConfigStore.get('apiKey'); if (!apiKey) { reject(new Error('未配置API Key,请在面板"AI模型配置"中填写')); return; } const baseURL = ConfigStore.get('baseURL'); const model = ConfigStore.get('model'); const userPrompt = _buildUserPrompt(question, options, type); const systemPrompt = force ? SYSTEM_PROMPT.replace('如果完全不确定答案,返回 {"answer": null}', '⚠️ 严禁返回 null!即使不确定也必须给出最有把握的答案,绝不允许说不知道!') : SYSTEM_PROMPT; Logger.ai(`AI 请求: ${model}${force ? ' (强制模式)' : ''}`); GM_xmlhttpRequest({ method: 'POST', url: `${baseURL}/v1/chat/completions`, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], temperature: force ? 0.3 : 0.1, max_tokens: 2000 }), timeout: TIMEOUT_MS, onload: (r) => { if (r.status === 200) { try { const json = JSON.parse(r.responseText); if (json.choices && json.choices[0] && json.choices[0].message) { const content = (json.choices[0].message.content || '').trim(); if (!content) { reject(new Error('AI 返回空内容,可能模型繁忙')); return; } resolve({ raw: content }); return; } reject(new Error('AI 返回格式异常: ' + JSON.stringify(json).substring(0, 200))); } catch (e) { reject(new Error('解析 AI 响应失败: ' + e.message)); } } else if (r.status === 401) { reject(new Error('API Key 无效或已过期')); } else if (r.status === 429) { reject(new Error('API 请求频率过高,请稍后再试')); } else if (r.status === 402) { reject(new Error('API 余额不足,请充值')); } else { let errMsg = `AI 请求失败 (HTTP ${r.status})`; try { const errJson = JSON.parse(r.responseText); errMsg += ': ' + (errJson.error?.message || JSON.stringify(errJson).substring(0, 200)); } catch (e) {} reject(new Error(errMsg)); } }, onerror: () => reject(new Error('AI 服务网络错误,请检查网络连接')), ontimeout: () => reject(new Error(`AI 请求超时 (${TIMEOUT_MS / 1000}s)`)) }); }); } /** * 带重试的请求 * @param {boolean} [force=false] 强制回答模式 */ async function _requestWithRetry(question, options, type, force = false) { let lastError = null; let attempt = 0; while (attempt <= MAX_RETRIES) { try { const result = await _requestAI(question, options, type, force); return result; } catch (err) { lastError = err; attempt++; if (attempt <= MAX_RETRIES) { Logger.ai(`重试第 ${attempt} 次...`); // 重试前等待递增 await new Promise(r => setTimeout(r, 1000 * attempt)); } } } throw lastError || new Error('AI 请求失败(已达最大重试次数)'); } // ---------- JSON 解析与校验 ---------- /** * 从模型返回的文本中提取 JSON * @param {string} raw * @returns {object|null} */ function _extractJSON(raw) { if (!raw) return null; let cleaned = raw; // 0) 去除 code fence 标记(```json ... ``` 或 ``` ... ```) const fenceMatch = cleaned.match(/```(?:json|javascript|js)?\s*([\s\S]*?)```/i); if (fenceMatch) { cleaned = fenceMatch[1].trim(); } // 尝试直接解析 let parsed = safeJSONParse(cleaned); if (parsed && (parsed.answer !== undefined || parsed.answers !== undefined)) { return parsed; } // 尝试用正则提取 JSON 块(非贪婪优先,避免带入多余内容) let jsonMatch = cleaned.match(/\{[\s\S]*?\}/); if (!jsonMatch) jsonMatch = cleaned.match(/\{[\s\S]*\}/); if (!jsonMatch) { // 回退到原始字符串尝试 jsonMatch = raw.match(/\{[\s\S]*?\}/); if (!jsonMatch) jsonMatch = raw.match(/\{[\s\S]*\}/); } if (jsonMatch) { let jsonStr = jsonMatch[0]; // 尝试修复常见的 JSON 问题 jsonStr = jsonStr .replace(/'([^']*)'\s*:/g, '"$1":') // 单引号 key → 双引号 .replace(/:\s*'([^']*)'/g, ': "$1"') // 单引号 value → 双引号 .replace(/,/g, ',') // 中文逗号 .replace(/:/g, ':') // 中文冒号 .replace(/[\n\r]+/g, ' ') // 换行 .replace(/,\s*}/g, '}'); // 尾部逗号 parsed = safeJSONParse(jsonStr); if (parsed && (parsed.answer !== undefined || parsed.answers !== undefined)) { return parsed; } // 二次尝试:如果 answer 字段是数组但期待的是多选,尝试转换 if (parsed && parsed.answer && Array.isArray(parsed.answer)) { return { answer: parsed.answer.join(''), answers: parsed.answer }; } if (parsed) { Logger.ai('JSON 解析成功但缺少 answer/answers 字段: ' + JSON.stringify(parsed).substring(0, 80)); } } // 兜底:尝试在原始文本中搜索 JSON-like 结构 const looseMatch = raw.match(/\{(?:[^{}]|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[\w-]+(?:\s*:\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,}]+))?[\s,]*)*\}/); if (looseMatch) { let looseStr = looseMatch[0] .replace(/'([^']*)'\s*:/g, '"$1":') .replace(/:\s*'([^']*)'/g, ': "$1"') .replace(/,/g, ',') .replace(/:/g, ':') .replace(/[\n\r]+/g, ' ') .replace(/,\s*}/g, '}'); parsed = safeJSONParse(looseStr); if (parsed && (parsed.answer !== undefined || parsed.answers !== undefined)) { return parsed; } } return null; } /** * 校验解析后的答案 * @param {object} json 解析后的 JSON * @param {string} type 题型 * @param {string[]} options 选项列表 * @returns {{answer: string|null, answers: string[]|null}} */ function _validateAnswer(json, type, options) { if (!json) return { answer: null, answers: null }; const optCount = options?.length || 0; switch (type) { case 'single': { let ans = json.answer; if (!ans && json.answers && Array.isArray(json.answers) && json.answers[0]) { ans = json.answers[0]; } if (ans && typeof ans === 'string') { ans = ans.trim().toUpperCase(); // 校验是否为合法选项 if (/^[A-D]$/i.test(ans)) { const idx = ans.charCodeAt(0) - 65; if (optCount === 0 || idx < optCount) { return { answer: ans, answers: [ans] }; } } // 尝试模糊匹配选项内容 for (let i = 0; i < optCount; i++) { if (options[i].includes(ans) || ans.includes(options[i])) { const letter = String.fromCharCode(65 + i); return { answer: letter, answers: [letter] }; } } // 返回原始值让上层处理 return { answer: ans, answers: [ans] }; } return { answer: null, answers: null }; } case 'multi': { let multiAns = json.answers; if (!multiAns && json.answer) { // 处理 "AB" / "A,B,C" / "A、B、C" 格式 const raw = String(json.answer); const letters = raw.match(/[A-D]/gi); multiAns = letters || []; } // 处理 answers 是字符串的情况(如 {"answers": "A,B,C"}) if (typeof multiAns === 'string') { const letters = multiAns.match(/[A-D]/gi); multiAns = letters || [multiAns.trim()]; } // 处理 answer 是数组的情况(如 {"answer": ["A", "C"]}) if (!multiAns && Array.isArray(json.answer)) { multiAns = json.answer; } if (Array.isArray(multiAns) && multiAns.length > 0) { const cleaned = [...new Set(multiAns.map(a => String(a).trim().toUpperCase()))] .filter(a => /^[A-D]$/.test(a)) .filter(a => optCount === 0 || (a.charCodeAt(0) - 65) < optCount) .sort(); if (cleaned.length > 0) { return { answer: cleaned.join(''), answers: cleaned }; } } return { answer: null, answers: null }; } case 'judge': { let ans = String(json.answer || ''); if (/^(对|正确|√|是|✓|T|true|1)$/i.test(ans)) return { answer: '对', answers: ['对'] }; if (/^(错|错误|×|否|✗|F|false|0)$/i.test(ans)) return { answer: '错', answers: ['错'] }; // 也尝试从 answers 数组 if (json.answers && Array.isArray(json.answers)) { ans = String(json.answers[0] || ''); if (/^(对|正确|√|是|✓|T|true|1)$/i.test(ans)) return { answer: '对', answers: ['对'] }; if (/^(错|错误|×|否|✗|F|false|0)$/i.test(ans)) return { answer: '错', answers: ['错'] }; } return { answer: null, answers: null }; } case 'fill': case 'short': { let fills = json.answers; if (!fills && json.answer) { fills = [json.answer]; } if (Array.isArray(fills) && fills.length > 0) { const cleaned = fills.map(f => String(f).trim()).filter(f => f); return { answer: cleaned.join(';'), answers: cleaned }; } return { answer: null, answers: null }; } default: return { answer: null, answers: null }; } } // ---------- 公开接口 ---------- /** * 调用 AI 大模型答题 * @param {string} question 题目文本 * @param {string} type 题型 'single'|'multi'|'judge'|'fill' * @param {string[]} [options=[]] 选项列表 * @returns {Promise<{success: boolean, answer: string|null, answers: string[]|null, raw: string, error: string|null}>} */ async function askAI(question, type = 'single', options = []) { const apiKey = ConfigStore.get('apiKey'); if (!apiKey) { return { success: false, answer: null, answers: null, raw: '', error: '未配置API Key,请在面板"AI模型配置"中填写' }; } const startTime = Date.now(); // 诊断日志:记录发送给 AI 的题干和选项 const optPreview = options.map((o, i) => `${String.fromCharCode(65 + i)}."${String(o).substring(0, 40)}"`).join(', '); Logger.ai(`📤 发送AI: q="${question.substring(0, 80)}" type=${type} opts=[${optPreview}]`); try { let { raw } = await _requestWithRetry(question, options, type); // 如果 AI 返回空响应,重试一次 if (!raw || raw.trim().length === 0) { Logger.ai('⚠ AI 返回空响应,2s 后重试...'); await new Promise(r => setTimeout(r, 2000)); const retry = await _requestWithRetry(question, options, type); raw = retry.raw; } const elapsed = Date.now() - startTime; const json = _extractJSON(raw); const validated = _validateAnswer(json, type, options); // 如果 AI 返回 null,用更强硬 prompt 重试一次 if (!validated.answer && (!validated.answers || validated.answers.length === 0) && json && json.answer === null) { Logger.ai('⚠ AI 返回 null,使用强制提示词重试...'); try { const { raw: raw2 } = await _requestWithRetry(question, options, type, true); // force=true const json2 = _extractJSON(raw2); const validated2 = _validateAnswer(json2, type, options); if (validated2.answer || (validated2.answers && validated2.answers.length > 0)) { Logger.ai(`✅ AI 重试成功: ${validated2.answer}`); EventBus.emit(EventBus.AI_SUCCESS, { question: question.substring(0, 40), answer: validated2.answer, elapsed: Date.now() - startTime }); return { success: true, answer: validated2.answer, answers: validated2.answers, raw: raw2, error: null }; } } catch (e) { Logger.ai(`重试失败: ${e.message}`); } } if (validated.answer || (validated.answers && validated.answers.length > 0)) { Logger.ai(`✅ AI 答案: ${validated.answer} (${(elapsed / 1000).toFixed(1)}s)`); EventBus.emit(EventBus.AI_SUCCESS, { question: question.substring(0, 40), answer: validated.answer, elapsed }); return { success: true, answer: validated.answer, answers: validated.answers, raw, error: null }; } else { // 详细日志帮助调试 const rawPreview = raw.substring(0, 200).replace(/\n/g, '↵'); let debugInfo = `type=${type}, optCount=${options.length}`; if (json) { debugInfo += `, jsonKeys=[${Object.keys(json).join(',')}]`; if (json.answer !== undefined) debugInfo += `, answer=${JSON.stringify(json.answer).substring(0, 60)}`; if (json.answers !== undefined) debugInfo += `, answers=${JSON.stringify(json.answers).substring(0, 60)}`; } else { debugInfo += `, json=null`; } Logger.ai(`⚠ AI 返回了答案但格式无法解析 [${debugInfo}]`); Logger.ai(` AI原文: ${rawPreview}`); EventBus.emit(EventBus.AI_FAILED, { reason: 'parse_error', raw: rawPreview }); return { success: false, answer: null, answers: null, raw, error: `AI 返回格式无法解析 (type=${type})` }; } } catch (err) { const elapsed = Date.now() - startTime; Logger.ai(`❌ AI 请求失败 (${(elapsed / 1000).toFixed(1)}s): ${err.message}`); EventBus.emit(EventBus.AI_FAILED, { reason: 'request_error', error: err.message }); return { success: false, answer: null, answers: null, raw: '', error: err.message }; } } /** * 检查 AI 是否已配置且可用 * @returns {boolean} */ function isConfigured() { return !!ConfigStore.get('apiKey'); } return { askAI, isConfigured }; })(); // ============================================================ // QuizSolver - 自动答题引擎 (v2.0 三路由精准匹配) // ============================================================ const QuizSolver = (function () { let _active = false; let _observer = null; let _solving = false; let _submitted = false; let _solvedCount = 0; let _totalCount = 0; let _skippedCount = 0; let _correctCount = 0; let _wrongCount = 0; let _retryTimer = null; /** 题库置信度阈值 */ const BANK_CONFIDENCE_THRESHOLD = 0.6; /** 检测测验是否已完成(任务点+页面批改标志) */ function _isQuizAlreadyDone() { try { // 1. 查顶层任务点 if (window.top && window.top !== window) { const doneIcons = window.top.document.querySelectorAll('.ans-job-icon[aria-label*="已完成"]'); if (doneIcons.length >= 2) return true; } // 2. 查当前页面批改标志(marking_dui/cuo) const markings = document.querySelectorAll('.marking_dui, .marking_cuo, .marking_half'); if (markings.length >= 2) return true; // 3. 查是否有"重新作答"按钮(意味着已提交过) if (document.querySelector('[onclick*="redo"], [onclick*="again"], .redo-btn')) return true; } catch (e) {} return false; } /** 点顶层"下一节" */ function _clickTopNextButton() { try { const topDoc = (window.top && window.top !== window) ? window.top.document : document; const nextBtn = topDoc.querySelector('#prevNextFocusNext, .next.fr, .prev_next.next'); if (nextBtn && nextBtn.offsetParent !== null) { nextBtn.click(); } } catch (e) {} } // ============ 通用工具 ============ /** 解密学习通加密字体(CipherFont) */ function _decodeCipherFont(doc) { try { const fontStyles = doc.querySelectorAll('style'); fontStyles.forEach(style => { const text = style.textContent || ''; // 检测 .font-cxsecret 等加密字体定义 if (/font-cxsecret|@font-face.*cxsecret/i.test(text)) { const fontEls = doc.querySelectorAll('.font-cxsecret, [class*="font-cx"]'); fontEls.forEach(el => { // 尝试读取实际渲染文本(浏览器已解码) const rendered = el.textContent; if (rendered) { el.setAttribute('data-decoded', rendered); } }); } }); } catch (e) { /* 静默处理 */ } } /** 检查题目是否已作答 */ function _isQuestionAnswered(container, pageType) { // 通用检查:已选中的 radio/checkbox const checkedInputs = container.querySelectorAll( 'input[aria-checked="true"], input:checked, .check_answer .check_answer_detail, .onChecked' ); if (checkedInputs.length > 0) return true; // 已选中状态的选项 li(各种 class 名) const activeOpts = container.querySelectorAll( '.cur, .selected, .active, .option_active, .answerBg.cur, [class*="checked"], li.check_answer' ); if (activeOpts.length > 0) return true; // 课程页内嵌测验:检查隐藏的 answer input 是否有值(如 answer404465570) // 排除 answertype/answeredView 等非答案标记 const answerHiddens = container.querySelectorAll('input[id^="answer"]:not([id*="type"]):not([id*="View"]):not([id*="isAnswered"])'); for (const h of answerHiddens) { if (h.value && h.value.trim() !== '' && !/^[0-9]+$/.test(h.value) && h.value !== 'false') return true; } // 判断题:检查 name="answer404465574" 这种隐藏 input // 排除 answertype 输入(仅标明题型,不是用户答案) const namedInputs = container.querySelectorAll('input[name^="answer"]:not([name*="type"]):not([name*="Editor"])'); for (const ni of namedInputs) { if (ni.type !== 'hidden') continue; const v = (ni.value || '').trim(); if (v && v !== 'false' && !/^[0-9]+$/.test(v)) return true; } // 填空题/简答题:检查输入框和编辑器是否有值 const inputs = container.querySelectorAll('input[type="text"], textarea'); for (const inp of inputs) { if (inp.value && inp.value.trim().length > 0) return true; } const editorAreas = container.querySelectorAll('.InpDIV, .edui-editor-iframeholder'); for (const area of editorAreas) { if (area.textContent && area.textContent.trim().length > 0) return true; } // 随堂练习专用 if (pageType === 'stuActive') { const activeLi = container.querySelector('.option-list li.active, .option-list li[class*="active"]'); if (activeLi) return true; } // 作业/考试专用 if (pageType === 'cxWork' || pageType === 'cxExam') { if (container.querySelector('.marking_dui, .marking_cuo, .marking_half')) return true; // 课程页测验专用:li 被点击后会有 check_answer 类 if (container.querySelector('li.check_answer, li.cur, .num_option_dx.check_answer')) return true; } return false; } /** 安全的模拟点击 */ function _safeClick(el) { if (!el) return; try { // 先滚动到可视区域 if (el.scrollIntoView) { el.scrollIntoView({ block: 'center', behavior: 'instant' }); } } catch (e) {} // 优先直接调用 onclick 处理程序(学习通 li 元素专用) try { if (el.onclick) { el.onclick.call(el); return; } } catch (e) {} // 备选:模拟完整点击事件序列 try { el.focus && el.focus(); ['mousedown', 'mouseup', 'click'].forEach(evtName => { el.dispatchEvent(new MouseEvent(evtName, { view: window, bubbles: true, cancelable: true })); }); el.click && el.click(); } catch (e) {} } /** 用 UEditor 填入内容(填空题/简答题的富文本编辑器) */ function _fillWithUEEditor(container, content) { try { // 考试格式:textarea[name^="answerEditor"] const textareas = container.querySelectorAll('textarea[name^="answerEditor"]'); for (const ta of textareas) { try { // 先通过 UE 编辑器 const ueId = ta.closest('.edui-default')?.querySelector('[id^="ueditor"]')?.id; if (ueId && typeof UE !== 'undefined' && UE.getEditor) { const editor = UE.getEditor(ueId); if (editor && editor.setContent) { editor.setContent(content); continue; } } // fallback:直接设 textarea 值 ta.value = content; ta.dispatchEvent(new Event('input', { bubbles: true })); ta.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} } const examTextareas = container.querySelectorAll('.textDIV, .subEditor'); if (examTextareas.length > 0 && content) { // 考试格式:通过 UEditor body 设置 const ueBody = container.querySelector('.edui-editor-body, .edui-editor-iframeholder iframe'); if (ueBody && ueBody.contentDocument) { ueBody.contentDocument.body.innerHTML = `

${content}

`; return true; } // 直接设文本 examTextareas.forEach(div => { div.textContent = content; div.dispatchEvent(new Event('input', { bubbles: true })); }); return textareas.length > 0 || examTextareas.length > 0; } // 常规 UEditor const editorHolders = container.querySelectorAll('[id^="ueditor"]'); for (const holder of editorHolders) { const id = holder.id || holder.getAttribute('name'); if (id && typeof UE !== 'undefined' && UE.getEditor) { const editor = UE.getEditor(id); if (editor && editor.setContent) { editor.setContent(content); return true; } } } // fallback:直接用 innerHTML 填入可视区域 const inpDiv = container.querySelector('.InpDIV, .ue-editor, .edui-editor-body'); if (inpDiv) { inpDiv.innerHTML = content; inpDiv.dispatchEvent(new Event('input', { bubbles: true })); inpDiv.dispatchEvent(new Event('change', { bubbles: true })); return true; } return false; } catch (e) { return false; } } /** 随机延时(模拟人工) */ function _randomDelay(baseMs = 800) { const extra = ConfigStore.get('simulateDelay') ? Math.random() * 1500 : 300; return new Promise(r => setTimeout(r, baseMs + extra)); } // ============ 分页面题目解析 ============ /** 随堂练习(answerQuestion2):查找题目容器 */ function _stuActive_findContainers() { return Array.from(document.querySelectorAll('.question-item, .questionBox, .TiMu')); } /** 随堂练习:提取题型 */ function _stuActive_detectType(container) { // 直接检查页面上的 class 标记 if (container.classList.contains('multiple-choice') || container.querySelector('.multiple-choice')) return 'multi'; if (container.classList.contains('single-choice') || container.querySelector('.single-choice')) return 'single'; if (container.classList.contains('true-or-false') || container.querySelector('.true-or-false')) return 'judge'; // 数 radio/checkbox const radios = container.querySelectorAll('input[type="radio"]'); const checks = container.querySelectorAll('input[type="checkbox"]'); const textarea = container.querySelectorAll('textarea, .InpDIV'); if (textarea.length > 0) return 'fill'; if (checks.length >= 2) return 'multi'; if (radios.length >= 2) return 'single'; // 从题目文本推断 const text = container.textContent.trim(); if (text.startsWith('【多选题】') || text.includes('多选题') || text.includes('多项选择')) return 'multi'; if (text.startsWith('【判断题】') || text.includes('判断题')) return 'judge'; if (text.startsWith('【填空题】') || text.includes('填空题')) return 'fill'; if (text.startsWith('【单选题】') || text.includes('单选题')) return 'single'; return 'single'; // 默认单选 } /** 随堂练习:提取题干 */ function _stuActive_extractText(container) { const titleEl = container.querySelector('.question-name, .q-title, .title, .question-title, .Cy_TItle, .Zy_TItle'); if (titleEl) { return titleEl.textContent.replace(/^\d+[\.\、\s]+/, '').trim(); } // 兜底:第一个 p 或 div 的文字 const firstP = container.querySelector('p'); if (firstP) return firstP.textContent.trim(); return container.textContent.replace(/\s+/g, ' ').trim().substring(0, 120); } /** 随堂练习:提取选项 */ function _stuActive_extractOptions(container) { const options = []; const optionItems = container.querySelectorAll('.option-list li, .options li, ul[class*="option"] li'); if (optionItems.length >= 2) { optionItems.forEach((item, idx) => { const resultEl = item.querySelector('.option-result, .option-content, span'); const text = resultEl ? resultEl.textContent.trim() : item.textContent.trim(); options.push({ text, el: item, letter: String.fromCharCode(65 + idx) }); }); return options; } // fallback:直接用 radio/label 组合 const inputs = container.querySelectorAll('input[type="radio"], input[type="checkbox"]'); inputs.forEach((input, idx) => { const wrapper = input.closest('li, label') || input.parentElement; options.push({ text: (wrapper || input.parentElement).textContent.trim(), el: input, letter: String.fromCharCode(65 + idx) }); }); return options; } /** 作业/考试(cxWork/cxExam):查找题目容器 */ function _cx_findContainers() { // 只用 .singleQuesId(.TiMu 在它内部,一起查会重复) const items = document.querySelectorAll('.singleQuesId'); if (items.length > 0) return Array.from(items); // 独立测验页面的备选容器 return Array.from(document.querySelectorAll('.questionLi, .exam_question')); } /** 作业/考试:提取题型 —— 适配课程页内嵌测验 + 考试页面 */ function _cx_detectType(container, pageType) { // 0) 最优先:从可见文字标签识别(.mark_name / .colorShallow / .type_tit) // 视觉标签比隐藏 input 更可靠,优先检查 const markName = container.querySelector('.mark_name'); const mnText = markName ? markName.textContent : ''; const colorShallow = container.querySelector('.colorShallow'); const shText = colorShallow ? colorShallow.textContent : ''; const typeTit = container.querySelector('.type_tit') || container.closest('.mark_table')?.querySelector('.type_tit'); const typeText = typeTit ? typeTit.textContent : ''; const visibleTypeText = mnText + shText + typeText; if (visibleTypeText.includes('多选') || visibleTypeText.includes('多项')) return 'multi'; if (visibleTypeText.includes('单选')) return 'single'; if (visibleTypeText.includes('判断')) return 'judge'; if (visibleTypeText.includes('填空') || visibleTypeText.includes('推理计算')) return 'fill'; if (visibleTypeText.includes('简答') || visibleTypeText.includes('论述') || visibleTypeText.includes('算法实现')) return 'short'; // 0.5) 考试:当文字标签无法识别时,从 hidden input name^="type" 取值 if (pageType === 'cxExam') { const typeInput = container.querySelector('input[name^="type"]:not([name="type"])'); if (typeInput) { const tv = parseInt(typeInput.value) || 0; if (tv === 0 || tv === 1) return 'single'; if (tv === 2) return 'fill'; if (tv === 3) return 'judge'; if (tv === 4) return 'multi'; if (tv >= 5) return 'short'; } } // 1) 从题目文字中的类型标签识别(最可靠) const typeTag = container.querySelector('.newZy_TItle'); if (typeTag) { const txt = typeTag.textContent.trim(); if (txt.includes('多选')) return 'multi'; if (txt.includes('判断')) return 'judge'; if (txt.includes('填空')) return 'fill'; if (txt.includes('简答')) return 'short'; if (txt.includes('单选')) return 'single'; } // 2) 从最近的 section header 识别 const sectionHeader = container.parentElement.querySelector('h3.newTestType'); if (sectionHeader) { const txt = sectionHeader.textContent.trim(); if (txt.includes('多选')) return 'multi'; if (txt.includes('判断')) return 'judge'; if (txt.includes('填空')) return 'fill'; if (txt.includes('简答')) return 'short'; if (txt.includes('单选')) return 'single'; } // 3) 从 li 的 qtype 属性识别 const firstLi = container.querySelector('li[qtype]'); if (firstLi) { const qt = parseInt(firstLi.getAttribute('qtype')) || 0; if (qt === 1) { // qtype=1 可能是单选也可能是多选,看 onclick 函数名区分 if (/addMultipleChoice/i.test(firstLi.getAttribute('onclick') || '')) return 'multi'; return 'single'; } if (qt === 2) return 'multi'; if (qt === 3) return 'judge'; if (qt === 4) return 'fill'; if (qt === 5 || qt === 6 || qt === 7) return 'short'; } // 4) fallback const checks = container.querySelectorAll('input[type="checkbox"]'); const radios = container.querySelectorAll('input[type="radio"]'); if (checks.length >= 2) return 'multi'; if (radios.length >= 2) return 'single'; return 'single'; } /** 作业/考试:提取题干 —— 适配课程页内嵌测验 + 考试页面 */ function _cx_extractText(container) { // 考试专用:.mark_name 全部文本(含代码块) const examH3 = container.querySelector('.mark_name'); if (examH3) { const fullText = examH3.textContent.replace(/\s+/g, ' ').trim(); return fullText.substring(0, 500); } // 优先用 .Zy_TItle 内的 .fontLabel const titleEl = container.querySelector('.Zy_TItle, .Cy_TItle, .TiMu-title, .question-stem'); if (titleEl) { // 去掉题号和【题型标签】,保留纯题干 return titleEl.textContent .replace(/^\d+[\.\、\s]+/, '') .replace(/【[^】]+】/g, '') .trim(); } return container.textContent.replace(/\s+/g, ' ').trim().substring(0, 150); } /** 作业/考试:提取选项 —— 适配 .Zy_ulTop > li 结构(有 onclick 的 li) */ function _cx_extractOptions(container) { const options = []; // 方式1:ul.Zy_ulTop > li(课程页内嵌测验/章节测试) const zyItems = container.querySelectorAll('ul.Zy_ulTop > li'); if (zyItems.length >= 2) { zyItems.forEach((item, idx) => { const label = item.querySelector('label span, span[name]'); const textEl = item.querySelector('a.after, a'); const letter = label ? (label.getAttribute('data') || label.textContent.trim()) : String.fromCharCode(65 + idx); let text = textEl ? textEl.textContent.trim() : item.textContent.replace(/\s+/g, '').trim().substring(0, 50); // 如果提取的文本就是单个字母,说明实际内容在别处 if (text.length <= 2 && /^[A-F]$/i.test(text)) { const allText = item.textContent.replace(/\s+/g, ' ').trim(); text = allText.replace(/^[A-F][.、.\s]*/, '').trim() || allText; } options.push({ text: text, el: item, letter: letter, dataVal: label ? label.getAttribute('data') : null }); }); // 如果所有选项文本都是单字母,降级走后面的分支 if (options.every(o => o.text.length <= 2 && /^[A-F]$/i.test(o.text))) { options.length = 0; } else { return options; } } // 方式1.5:考试页面 checkbox/radio 结构(.num_option 或 label 包裹的 input) const checkRadios = container.querySelectorAll('input[type="radio"], input[type="checkbox"]'); if (checkRadios.length >= 2) { checkRadios.forEach((input, idx) => { let text = ''; // a) 优先找关联的 label[for](如 ) if (input.id) { const label = container.querySelector(`label[for="${CSS.escape(input.id)}"]`); if (label) text = label.textContent; } // b) 找包裹 input 的 label if (!text || text.length < 3) { const label = input.closest('label'); if (label) text = label.textContent; } // c) 找 .answerBg 父容器,取其内文本区域 if (!text || text.length < 3) { const answerBg = input.closest('.answerBg'); if (answerBg) { const textArea = answerBg.querySelector('.answer_p, .answer_content, p, .fontLabel, .single_prefix, span:not(.num_option_dx):not(.num_option)'); text = textArea ? textArea.textContent : answerBg.textContent; } } // d) 找父元素文本 if (!text || text.length < 3) { const parent = input.parentElement; if (parent) text = parent.textContent; // 如果父元素文本太短,试兄弟节点 if (text.length < 3 && input.nextElementSibling) { text = input.nextElementSibling.textContent || ''; } } // 清理:去掉开头字母编号、多余空白 text = text.replace(/^\s*[A-F][.、.\s]*/i, '').trim() || text.trim(); if (!text) text = `选项${String.fromCharCode(65 + idx)}`; options.push({ text: text.substring(0, 120), el: input, letter: String.fromCharCode(65 + idx) }); }); if (options.length >= 2) return options; } // 方式2:.answerBg(考试/作业页面 div 结构,含 num_option_dx + answer_p) const answerItems = container.querySelectorAll('.answerBg'); if (answerItems.length >= 2) { answerItems.forEach((item, idx) => { // .answer_p 优先取;不用 span,避免抓到字母标签 const pEl = item.querySelector('.answer_p, p') || item; let text = pEl.textContent.trim(); // 去掉开头可能残留的字母编号 text = text.replace(/^[A-F][.、.\s]*/, '').trim() || text; options.push({ text: text, el: item, letter: String.fromCharCode(65 + idx) }); }); return options; } // 终极 fallback: 遍历所有 radio/checkbox 用父元素文本 const allInputs = container.querySelectorAll('input[type="radio"], input[type="checkbox"]'); allInputs.forEach((input, idx) => { const wrapper = input.closest('li, label, .answerBg') || input.parentElement; let text = (wrapper || {}).textContent || ''; text = text.replace(/^\s*[A-F][.、.\s]*/i, '').trim() || text.trim(); options.push({ text: text.substring(0, 80), el: input, letter: String.fromCharCode(65 + idx) }); }); return options; } // ============ 分页面答案填入 ============ /** 随堂练习:填入答案 */ function _stuActive_fill(container, type, answer, answers, options) { if (!answer && (!answers || answers.length === 0)) return false; const fillFn = () => { switch (type) { case 'multi': { // 多选题:先取消所有已勾选,再勾选正确选项 const checkboxes = container.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(cb => { if (cb.checked) { cb.checked = false; cb.dispatchEvent(new Event('change', { bubbles: true })); } }); // 点击正确选项的 li const letters = (answer || '').match(/[A-H]/gi) || (answers || []); let clicked = false; for (const l of letters) { const idx = (typeof l === 'string' ? l.charCodeAt(0) : String(l).charCodeAt(0)) - 65; if (idx >= 0 && options[idx]) { _safeClick(options[idx].el); clicked = true; } } return clicked; } case 'judge': case 'single': { const letter = (answer || '').toUpperCase().charAt(0); const idx = letter.charCodeAt(0) - 65; if (idx >= 0 && options[idx]) { _safeClick(options[idx].el); return true; } // 文字匹配 for (const opt of options) { const ansText = opt.text.replace(/\s/g, ''); const tgtText = (answer || '').replace(/\s/g, ''); if (ansText.includes(tgtText) || tgtText.includes(ansText)) { _safeClick(opt.el); return true; } } return false; } case 'fill': case 'short': { const fillVals = answers || (answer ? [answer] : []); // 考试多空:逐个填入(UEditor / CodeMirror / textarea) const ueTextareas = container.querySelectorAll('textarea[name^="answerEditor"]'); const cmEditors = container.querySelectorAll('.CodeMirror'); const totalBlanks = Math.max(ueTextareas.length, cmEditors.length); if (totalBlanks > 0) { let realFills = fillVals.slice(); if (realFills.length < totalBlanks && realFills.length > 0) { const last = realFills[realFills.length - 1]; const split = last.split(/[;;,,]+/).map(s => s.trim()).filter(Boolean); if (split.length >= totalBlanks) { realFills = split; } } let filled = 0; ueTextareas.forEach((ta, i) => { const val = realFills[i] || realFills[realFills.length - 1] || '0'; try { // textarea 的 id 就是 UEditor 实例名 if (ta.id && typeof UE !== 'undefined' && UE.getEditor) { const editor = UE.getEditor(ta.id); if (editor && editor.setContent) { editor.setContent('

' + val + '

'); filled++; return; } } ta.value = val; ta.dispatchEvent(new Event('input', { bubbles: true })); ta.dispatchEvent(new Event('change', { bubbles: true })); filled++; } catch (e) {} }); cmEditors.forEach((cmEl, i) => { const val = realFills[ueTextareas.length + i] || realFills[realFills.length - 1] || '0'; try { if (cmEl.CodeMirror && cmEl.CodeMirror.setValue) { cmEl.CodeMirror.setValue(val); filled++; } } catch (e) {} }); return filled > 0; } if (_fillWithUEEditor(container, fillVals.join('
'))) return true; const inputs = container.querySelectorAll('input[type="text"], input:not([type="radio"]):not([type="checkbox"]):not([type="submit"]), textarea'); for (let i = 0; i < Math.min(inputs.length, fillVals.length); i++) { const inp = inputs[i]; inp.focus(); inp.value = fillVals[i]; inp.dispatchEvent(new Event('input', { bubbles: true })); inp.dispatchEvent(new Event('change', { bubbles: true })); inp.blur(); } return inputs.length > 0; } default: return false; } }; return fillFn(); } /** 作业/考试:填入答案 —— 点击 li 触发 onclick="addChoice/addMultipleChoice(this)" */ function _cx_fill(container, type, answer, answers, options) { if (!answer && (!answers || answers.length === 0)) return false; const fillFn = () => { switch (type) { case 'multi': { // 先取消已勾选的选项(点击它们取消勾选) const allLis = container.querySelectorAll('ul.Zy_ulTop > li'); allLis.forEach(li => { // 已选中的 li 会有特定 class,取消它 if (li.classList.contains('cur') || li.classList.contains('check_answer') || li.querySelector('.check_answer_detail')) { _safeClick(li); } }); // 点击正确选项 const letters = (answer || '').match(/[A-H]/gi) || (answers || []); let clicked = false; for (const l of letters) { const idx = (typeof l === 'string' ? l.charCodeAt(0) : String(l).charCodeAt(0)) - 65; if (idx >= 0 && options[idx]) { _safeClick(options[idx].el); // el 是 li clicked = true; } } return clicked; } case 'single': case 'judge': { const letter = (answer || '').toUpperCase().charAt(0); const idx = letter.charCodeAt(0) - 65; if (idx >= 0 && options[idx]) { _safeClick(options[idx].el); return true; } // 判断题专用:按 dataVal 匹配 'true'/'false' if (type === 'judge') { const targetVal = /^(对|正确|√|是|✓|T|true|1)$/i.test(answer || '') ? 'true' : 'false'; for (const opt of options) { if (opt.dataVal === targetVal) { _safeClick(opt.el); return true; } } // 位置 fallback:没有 data 属性时,索引0=对,索引1=错 if (!options.some(o => o.dataVal)) { const idx = targetVal === 'true' ? 0 : 1; if (options[idx]) { _safeClick(options[idx].el); return true; } } } // 文字匹配 fallback for (const opt of options) { if (opt.text.includes(answer) || (answer || '').includes(opt.text.substring(0, 3))) { _safeClick(opt.el); return true; } } return false; } case 'fill': case 'short': { const fillVals = answers || (answer ? [answer] : []); // 考试多空:逐个填入(UEditor / CodeMirror / textarea) const ueTextareas = container.querySelectorAll('textarea[name^="answerEditor"]'); const cmEditors = container.querySelectorAll('.CodeMirror'); const totalBlanks = Math.max(ueTextareas.length, cmEditors.length); if (totalBlanks > 0) { let realFills = fillVals.slice(); if (realFills.length < totalBlanks && realFills.length > 0) { const last = realFills[realFills.length - 1]; const split = last.split(/[;;,,]+/).map(s => s.trim()).filter(Boolean); if (split.length >= totalBlanks) { realFills = split; } } let filled = 0; ueTextareas.forEach((ta, i) => { const val = realFills[i] || realFills[realFills.length - 1] || '0'; try { if (ta.id && typeof UE !== 'undefined' && UE.getEditor) { const editor = UE.getEditor(ta.id); if (editor && editor.setContent) { editor.setContent('

' + val + '

'); filled++; return; } } ta.value = val; ta.dispatchEvent(new Event('input', { bubbles: true })); ta.dispatchEvent(new Event('change', { bubbles: true })); filled++; } catch (e) {} }); cmEditors.forEach((cmEl, i) => { const val = realFills[ueTextareas.length + i] || realFills[realFills.length - 1] || '0'; try { if (cmEl.CodeMirror && cmEl.CodeMirror.setValue) { cmEl.CodeMirror.setValue(val); filled++; } } catch (e) {} }); return filled > 0; } if (_fillWithUEEditor(container, fillVals.join('
'))) return true; const inputs = container.querySelectorAll('input[type="text"], input:not([type="radio"]):not([type="checkbox"]):not([type="submit"]):not([type="hidden"]), textarea, .fill-input'); for (let i = 0; i < Math.min(inputs.length, fillVals.length); i++) { const inp = inputs[i]; inp.focus(); if (ConfigStore.get('simulateDelay') && fillVals[i].length < 20) { inp.value = ''; for (const ch of fillVals[i]) { inp.value += ch; inp.dispatchEvent(new Event('input', { bubbles: true })); } } else { inp.value = fillVals[i]; inp.dispatchEvent(new Event('input', { bubbles: true })); } inp.dispatchEvent(new Event('change', { bubbles: true })); inp.blur(); } return inputs.length > 0; } default: return false; } }; return fillFn(); } // ============ 答案正确性检测 ============ /** 检测并统计答题结果 */ function _detectAndReportResults(pageType) { try { // 等 2 秒让页面渲染批改结果 setTimeout(() => { const correctEls = document.querySelectorAll( '.marking_dui, .text-success, .dui, .correct, .right-answer, .question-right' ); const wrongEls = document.querySelectorAll( '.marking_cuo, .text-danger, .cuo, .wrong, .incorrect, .question-wrong, .marking_half' ); _correctCount = correctEls.length; _wrongCount = wrongEls.length; if (_correctCount > 0 || _wrongCount > 0) { const rate = _correctCount + _wrongCount > 0 ? ((_correctCount / (_correctCount + _wrongCount)) * 100).toFixed(1) : 'N/A'; Logger.success(`✅ 答题结果:正确 ${_correctCount} / 错误 ${_wrongCount} (正确率 ${rate}%)`); EventBus.emit('quiz:result', { correct: _correctCount, wrong: _wrongCount, rate: rate + '%' }); } }, 2500); } catch (e) {} } // ============ 兜底答案 ============ /** 无答案时的兜底策略:单C / 判对 / 多全选 / 填0 */ function _getFallbackAnswer(type, options) { switch (type) { case 'single': { const idx = options.length >= 3 ? 2 : Math.max(0, options.length - 1); return String.fromCharCode(65 + idx); } case 'judge': return '对'; case 'multi': { const letters = options.map((_, i) => String.fromCharCode(65 + i)); return letters.join(''); } case 'fill': case 'short': return '0'; default: return null; } } function _getFallbackAnswers(type, options) { switch (type) { case 'single': { const idx = options.length >= 3 ? 2 : Math.max(0, options.length - 1); return [String.fromCharCode(65 + idx)]; } case 'judge': return ['对']; case 'multi': return options.map((_, i) => String.fromCharCode(65 + i)); case 'fill': case 'short': return ['0']; default: return null; } } // ============ 自动提交 ============ function _tryAutoSubmit(pageType) { if (!ConfigStore.get('autoSubmit')) return; const selectors = pageType === 'stuActive' ? ['.submit-btn', '.submitBtn', '.sub_btn', 'button.submit', '.question-submit'] : pageType === 'cxExam' ? [ // 考试专用:交卷按钮 '.jb_btn.jb_btn_92.confirm', '.jb_btn.confirm', '[onclick*="submitTest"]', '[onclick*="reVersionSubmit"]', '.tijiao-btn', '.submit-exam', '.confirm-btn', // 通用 '.sub_btn', '.submitBtn', 'input[type="submit"]', 'button[type="submit"]' ] : [ // 课程页内嵌测验 '.btnSubmit.workBtnIndex', '.ZY_sub .btnSubmit', // 通用 '.sub_btn', '.submitBtn', '.ans-submit', '.tijiao-btn', '.Zy_btn', '.Cy_btn', 'a.btnSubmit', 'input[type="submit"]', 'button[type="submit"]', '.submit-exam', '.confirm-btn' ]; for (const sel of selectors) { try { const btn = document.querySelector(sel); if (btn && btn.offsetParent !== null) { Logger.quiz(`🤖 自动提交答卷...`); _submitted = true; _safeClick(btn); // 弹窗确认:学习通弹出 workPop 或 #submitConfirmPop 确认框 const _clickConfirm = () => { try { if (window.top && window.top !== window) { // workPop 弹窗 window.top.eval('if(window.$){$("#popok").trigger("click")}'); // 考试确认弹窗 #submitConfirmPop window.top.eval('if(window.$){$("#submitConfirmPop .jb_btn.confirm").trigger("click")}'); } } catch (e) {} // 备选:当前页面直接点 try { const confirmBtns = document.querySelectorAll('#submitConfirmPop .jb_btn.confirm, #popok, .jb_btn.confirm'); confirmBtns.forEach(b => { if (b.offsetParent !== null) b.click(); }); } catch (e) {} }; // 弹窗1 setTimeout(() => { _clickConfirm(); Logger.quiz('🤖 确认弹窗已自动提交'); }, 1000); // 弹窗2 setTimeout(() => { _clickConfirm(); }, 2500); // 轮询等待"下一节"按钮出现后自动点击(最长等 20s) let nextPoll = 0, _inlineClicked = 0; // 0=未点, >0=点了的时间戳 const MAX_NEXT_POLL = 40; // 40 * 500ms = 20s const _pollNextSection = setInterval(() => { nextPoll++; try { if (window.top && window.top !== window) { const topDoc = window.top.document; // 1. 弹窗中的"下一节"按钮(点内联"下一节"后可能弹出,等 3s 冷却) if (_inlineClicked && Date.now() - _inlineClicked > 3000) { const popupBtn = topDoc.querySelector('.nextChapter, a[class*="nextChapter"], a[onclick*="closeDeleteWindow"]'); if (popupBtn && popupBtn.offsetParent !== null) { popupBtn.click(); Logger.quiz('🤖 弹窗"下一节"已点击'); clearInterval(_pollNextSection); return; } } // 2. 内联"下一节"按钮(成绩页面上) if (!_inlineClicked) { const nextBtn = topDoc.querySelector( '#prevNextFocusNext, .next.fr, .prev_next.next, ' + '.jb_btn.next, div[onclick*="PCount.next"]:not([class*="preChapter"])' ); if (nextBtn && nextBtn.offsetParent !== null) { nextBtn.click(); Logger.quiz('🤖 自动跳转下一节'); _inlineClicked = Date.now(); } } } } catch (e) {} if (nextPoll >= MAX_NEXT_POLL) { clearInterval(_pollNextSection); } }, 500); return; } } catch (e) {} } } // ============ 自动跳转下一节 ============ function _autoNextSection() { if (!ConfigStore.get('autoSubmit')) return; // 提交+确认弹窗约需 1-2s,等成绩渲染出来再 3s 后点击"下一节" setTimeout(() => { try { const parentDoc = window.parent ? window.parent.document : null; if (!parentDoc) return; const nextBtn = parentDoc.querySelector( '#prevNextFocusNext, .next.fr, .prev_next.next, ' + '.jb_btn.next, div[onclick*="PCount.next"]' ); if (nextBtn && nextBtn.offsetParent !== null) { nextBtn.click(); Logger.quiz('🤖 自动跳转下一节'); } } catch (e) {} }, 3000); } // ============ 三个页面处理器 ============ /** 处理器1:随堂练习 (answerQuestion2) */ async function _handleStuActive() { const logStore = []; Logger.quiz('📖 进入随堂练习答题页面'); Logger.quiz('等待 Vue 渲染题目...'); // 轮询等待 .question-item 渲染(最多50次×500ms=25秒) let containers = _stuActive_findContainers(); let retry = 0; while (containers.length === 0 && retry < 50) { await new Promise(r => setTimeout(r, 500)); containers = _stuActive_findContainers(); retry++; } if (containers.length === 0) { Logger.warn('未检测到随堂练习题目'); return; } _totalCount = containers.length; _solvedCount = 0; _skippedCount = 0; Logger.quiz(`解析到 ${_totalCount} 道题目`); const skipAnswered = ConfigStore.get('skipAnswered'); for (let i = 0; i < containers.length; i++) { if (!_active) break; const container = containers[i]; // 跳过已答 if (skipAnswered && _isQuestionAnswered(container, 'stuActive')) { _skippedCount++; continue; } const type = _stuActive_detectType(container); const question = _stuActive_extractText(container); const options = _stuActive_extractOptions(container); if (!question || question.length < 2) continue; Logger.quiz(`[${i + 1}/${_totalCount}] ${question.substring(0, 30)}... [${type}]`); PanelMgr.setProgress(`${i + 1}/${_totalCount}`); // 题库搜索 const optTexts = options.map(o => o.text); let answer = null, answers = null, source = ''; const bankResult = await AnswerSearcher.search(question, type, optTexts); if (bankResult.answer && bankResult.confidence >= BANK_CONFIDENCE_THRESHOLD) { answer = bankResult.answer; answers = bankResult.answers; source = 'bank'; Logger.quiz(`📚 题库: ${answer} (${(bankResult.confidence * 100).toFixed(0)}%)`); } else if (ConfigStore.get('aiEnable') && AISolver.isConfigured()) { Logger.quiz('🤖 AI 推理中...'); const aiResult = await AISolver.askAI(question, type, optTexts); if (aiResult.success && aiResult.answer) { answer = aiResult.answer; answers = aiResult.answers; source = 'ai'; Logger.quiz(`🤖 AI: ${answer}`); } } else if (bankResult.answer) { answer = bankResult.answer; answers = bankResult.answers; source = 'bank_low'; } if (answer || (answers && answers.length > 0)) { const filled = _stuActive_fill(container, type, answer, answers, options); if (filled) { _solvedCount++; EventBus.emit(EventBus.QUIZ_SOLVED, { question: question.substring(0, 40), answer: answer, source: source }); } } else { EventBus.emit(EventBus.QUIZ_FAILED, { question: question.substring(0, 40) }); } // 题目间延时 await _randomDelay(ConfigStore.get('answerInterval') * 1000); } Logger.success(`随堂练习完成:解答 ${_solvedCount} / 跳过 ${_skippedCount} / 总计 ${_totalCount}`); _tryAutoSubmit('stuActive'); _detectAndReportResults('stuActive'); if (_totalCount > 0) { _solving = false; setTimeout(() => stop(), 3000); } } /** 处理器2:作业/章节测试 (/mooc2/work/*) */ async function _handleCxWork() { if (_submitted) { Logger.quiz('⚠ 已提交,跳过重复处理'); return; } // 立即检测:任务点已完成 → 直接下一节 if (_isQuizAlreadyDone()) { Logger.quiz('📌 测验已完成,跳转下一节'); _clickTopNextButton(); return; } Logger.quiz('📝 进入作业/章节测试页面'); Logger.quiz('等待题目加载(5s)...'); await new Promise(r => setTimeout(r, 5000)); // 解密字体 _decodeCipherFont(document); let containers = _cx_findContainers(); let retry = 0; while (containers.length === 0 && retry < 30) { await new Promise(r => setTimeout(r, 1000)); containers = _cx_findContainers(); retry++; } if (containers.length === 0) { Logger.warn('未检测到作业题目'); return; } _totalCount = containers.length; _solvedCount = 0; _skippedCount = 0; Logger.quiz(`解析到 ${_totalCount} 道题目`); const skipAnswered = ConfigStore.get('skipAnswered'); for (let i = 0; i < containers.length; i++) { if (!_active) break; const container = containers[i]; if (skipAnswered && _isQuestionAnswered(container, 'cxWork')) { _skippedCount++; continue; } const type = _cx_detectType(container, 'cxWork'); const question = _cx_extractText(container); const options = _cx_extractOptions(container); if (!question || question.length < 2) continue; Logger.quiz(`[${i + 1}/${_totalCount}] ${question.substring(0, 30)}... [${type}]`); PanelMgr.setProgress(`${i + 1}/${_totalCount}`); const optTexts = options.map(o => o.text); let answer = null, answers = null, source = ''; const bankResult = await AnswerSearcher.search(question, type, optTexts); if (bankResult.answer && bankResult.confidence >= BANK_CONFIDENCE_THRESHOLD) { answer = bankResult.answer; answers = bankResult.answers; source = 'bank'; Logger.quiz(`📚 题库: ${answer} (${(bankResult.confidence * 100).toFixed(0)}%)`); } else if (ConfigStore.get('aiEnable') && AISolver.isConfigured()) { Logger.quiz('🤖 AI 推理中...'); const aiResult = await AISolver.askAI(question, type, optTexts); if (aiResult.success && aiResult.answer) { answer = aiResult.answer; answers = aiResult.answers; source = 'ai'; Logger.quiz(`🤖 AI: ${answer}`); } } else if (bankResult.answer) { answer = bankResult.answer; answers = bankResult.answers; source = 'bank_low'; } if (answer || (answers && answers.length > 0)) { const filled = _cx_fill(container, type, answer, answers, options); if (filled) { _solvedCount++; EventBus.emit(EventBus.QUIZ_SOLVED, { question: question.substring(0, 40), answer: answer, source: source }); } else { Logger.warn(`填入失败 [${type}]: 答案=${answer} 选项数=${options.length}`); } } else { // 兜底策略:AI/题库都答不出来时用默认答案 answer = _getFallbackAnswer(type, options); answers = _getFallbackAnswers(type, options); if (answer || (answers && answers.length > 0)) { const filled = _cx_fill(container, type, answer, answers, options); if (filled) { _solvedCount++; Logger.quiz(`🔧 兜底 ${type}: ${answer}`); } } EventBus.emit(EventBus.QUIZ_FAILED, { question: question.substring(0, 40) }); } await _randomDelay(ConfigStore.get('answerInterval') * 1000); } Logger.success(`作业完成:解答 ${_solvedCount} / 跳过 ${_skippedCount} / 总计 ${_totalCount}`); if (_solvedCount > 0) _tryAutoSubmit('cxWork'); _detectAndReportResults('cxWork'); if (_totalCount > 0) { _solving = false; setTimeout(() => stop(), 3000); } } /** 处理器3:考试 (/exam-ans/) */ async function _handleCxExam() { if (_submitted) { Logger.quiz('⚠ 已提交,跳过重复处理'); return; } if (_isQuizAlreadyDone()) { Logger.quiz('📌 考试已完成,跳转下一节'); _clickTopNextButton(); return; } Logger.quiz('📋 进入考试页面'); Logger.quiz('等待题目加载(5s)...'); await new Promise(r => setTimeout(r, 5000)); _decodeCipherFont(document); let containers = _cx_findContainers(); let retry = 0; while (containers.length === 0 && retry < 40) { await new Promise(r => setTimeout(r, 1000)); containers = _cx_findContainers(); retry++; } if (containers.length === 0) { Logger.warn('未检测到考试题目'); return; } _totalCount = containers.length; _solvedCount = 0; _skippedCount = 0; Logger.quiz(`解析到 ${_totalCount} 道题目`); const skipAnswered = ConfigStore.get('skipAnswered'); for (let i = 0; i < containers.length; i++) { if (!_active) break; const container = containers[i]; if (skipAnswered && _isQuestionAnswered(container, 'cxExam')) { _skippedCount++; continue; } const type = _cx_detectType(container, 'cxExam'); let question = _cx_extractText(container); const options = _cx_extractOptions(container); // 考试填空/简答:检测空位数 let blankCount = 1; if (type === 'fill' || type === 'short') { const textareas = container.querySelectorAll('textarea[name^="answerEditor"]'); blankCount = Math.max(1, textareas.length); if (blankCount > 1) { question += ` (共${blankCount}个空,请按顺序给出答案)`; } } if (!question || question.length < 2) continue; Logger.quiz(`[${i + 1}/${_totalCount}] ${question.substring(0, 30)}... [${type}]`); PanelMgr.setProgress(`${i + 1}/${_totalCount}`); const optTexts = options.map(o => o.text); let answer = null, answers = null, source = ''; const bankResult = await AnswerSearcher.search(question, type, optTexts); if (bankResult.answer && bankResult.confidence >= BANK_CONFIDENCE_THRESHOLD) { answer = bankResult.answer; answers = bankResult.answers; source = 'bank'; Logger.quiz(`📚 题库: ${answer} (${(bankResult.confidence * 100).toFixed(0)}%)`); } else if (ConfigStore.get('aiEnable') && AISolver.isConfigured()) { Logger.quiz('🤖 AI 推理中...'); const aiResult = await AISolver.askAI(question, type, optTexts); if (aiResult.success && aiResult.answer) { answer = aiResult.answer; answers = aiResult.answers; source = 'ai'; Logger.quiz(`🤖 AI: ${answer}`); } } else if (bankResult.answer) { answer = bankResult.answer; answers = bankResult.answers; source = 'bank_low'; } if (answer || (answers && answers.length > 0)) { const filled = _cx_fill(container, type, answer, answers, options); if (filled) { _solvedCount++; EventBus.emit(EventBus.QUIZ_SOLVED, { question: question.substring(0, 40), answer: answer, source: source }); // 考试专用:答完后点"下一题" setTimeout(() => { // 考试:用 href hack 或精确 onclick 匹配"下一题"(排除"上一题") const nextBtn = document.querySelector('[onclick="getTheNextQuestion(1)"]') || document.querySelector('.jb_btn[onclick*="getTheNextQuestion"]'); if (nextBtn && nextBtn.offsetParent !== null) { nextBtn.click(); Logger.quiz('📝 已点击下一题'); } }, 1500); } else { Logger.warn(`填入失败 [${type}]: 答案=${answer} 选项数=${options.length}`); } } else { // 兜底策略:AI/题库都答不出来时用默认答案 answer = _getFallbackAnswer(type, options); answers = _getFallbackAnswers(type, options); if (answer || (answers && answers.length > 0)) { const filled = _cx_fill(container, type, answer, answers, options); if (filled) { _solvedCount++; Logger.quiz(`🔧 兜底 ${type}: ${answer}`); } } EventBus.emit(EventBus.QUIZ_FAILED, { question: question.substring(0, 40) }); } await _randomDelay(ConfigStore.get('answerInterval') * 1000); // 考试翻页检测:每5题检查是否翻页 if ((i + 1) % 5 === 0 && i + 1 < containers.length) { const newContainers = _cx_findContainers(); if (newContainers.length > containers.length) { // 题目增加了(翻页了),更新容器列表 const oldLen = containers.length; containers = newContainers; _totalCount = containers.length; Logger.quiz(`检测到翻页,题目总数更新为 ${_totalCount}`); // 不 reset i,继续从当前位置 } } } Logger.success(`考试完成:解答 ${_solvedCount} / 跳过 ${_skippedCount} / 总计 ${_totalCount}`); if (_solvedCount > 0) _tryAutoSubmit('cxExam'); // 自动停止答题,防止用户翻回去检查答案时脚本乱改 if (_totalCount > 0) { Logger.quiz('🛑 考试答题已完成,自动停止以保护用户答案'); _solving = false; setTimeout(() => stop(), 3000); // 等3s确保提交弹窗自动确认完成 } _detectAndReportResults('cxExam'); } // ============ 主调度 ============ /** 根据URL选择处理器类型(与 detectPageType 保持同步) */ function _getPageHandlerType() { const url = location.href; if (url.includes('answerQuestion2')) return 'stuActive'; if (url.includes('/exam-ans/') || url.includes('/exam/') || /test\//i.test(url)) return 'cxExam'; if (url.includes('/mooc2/work/') || url.includes('/work/') || url.includes('/homework/') || url.includes('/quiz/')) return 'cxWork'; // 课程页面内嵌章节测验(视频和测验是同一URL下的标签切换,URL不变) if (/\/mycourse\/|courseid/i.test(url)) { const quizEls = document.querySelectorAll('.question-item, .questionLi, .TiMu, .mark_item, .answerBg, .singleQuesId'); // 必须测验元素存在且可见(防止预加载的隐藏DOM被误判) const visibleQuiz = Array.from(quizEls).some(el => el.offsetParent !== null); if (visibleQuiz) { if (document.querySelectorAll('.question-item').length > 0) return 'stuActive'; return 'cxWork'; } return 'pending'; // 课程页面,测验标签未激活,静默等待 } return null; } async function _solveByPageType() { if (_solving) return; _solving = true; // 安全防线:已经提交过或处理完成,不再重复答题 // 注:首次加载即使已有批改标记(提前做过的测验),也需放行让处理器检测并点下一节 if (_submitted) { Logger.warn('🛑 已提交过,停止自动答题以保护用户答案'); stop(); _solving = false; return; } const pageType = _getPageHandlerType(); if (!pageType || pageType === 'pending') { // 课程页面测验未加载,或无法识别 → 等待下次触发 _solving = false; return; } Logger.quiz(`识别页面类型: ${pageType}`); try { switch (pageType) { case 'stuActive': await _handleStuActive(); break; case 'cxWork': await _handleCxWork(); break; case 'cxExam': await _handleCxExam(); break; } } catch (e) { Logger.error(`答题过程出错: ${e.message}`); console.error(e); } _solving = false; } // ============ DOM 监听 & 公共 API ============ function _startObserver() { if (_observer) _observer.disconnect(); _observer = new MutationObserver(debounce((mutations) => { if (!_active || _solving) return; const hasNew = mutations.some(m => Array.from(m.addedNodes).some(node => node.nodeType === 1 && ( node.querySelector('.question-item, .questionLi, .TiMu, .answerBg') || /question-item|questionLi|TiMu/.test(node.className || '') ) ) ); if (hasNew) { _solveByPageType(); } }, 1500)); _observer.observe(document.body, { childList: true, subtree: true }); } function start() { if (_active) return; _active = true; _solvedCount = 0; _totalCount = 0; _skippedCount = 0; _correctCount = 0; _wrongCount = 0; _solving = false; _startObserver(); // 轮询检测 URL 页面类型,识别到后直接交给 Handler(Handler 内部自带轮询等待 DOM) let attempts = 0; const MAX_ATTEMPTS = 30; function trySolve() { if (!_active || _solving) return; const pageType = _getPageHandlerType(); if (!pageType) { // 页面类型无法识别 → 等待 URL 跳转 attempts++; if (attempts >= MAX_ATTEMPTS) { Logger.warn('等待答题页面超时(未识别到答题页面URL)'); if (_retryTimer) { clearInterval(_retryTimer); _retryTimer = null; } } return; } if (pageType === 'pending') { // 课程页面,测验标签未激活 → 静默等待,不超时 // MutationObserver 会在测验DOM出现时自动触发 _solveByPageType return; } // 页面类型已识别 → 直接交给 Handler // (Handler 内部有独立的轮询等待逻辑,最多等待 25~40 秒) Logger.quiz(`识别页面类型: ${pageType},启动答题处理器...`); _solveByPageType(); if (_retryTimer) { clearInterval(_retryTimer); _retryTimer = null; } } setTimeout(trySolve, 2000); _retryTimer = setInterval(trySolve, 2500); Logger.success('自动答题已启动(等待题目加载...)'); } function stop() { if (!_active) return; _active = false; if (_observer) { _observer.disconnect(); _observer = null; } if (_retryTimer) { clearInterval(_retryTimer); _retryTimer = null; } _solving = false; Logger.warn('自动答题已停止'); } function isActive() { return _active; } // ---- 事件监听 ---- EventBus.on(EventBus.CONFIG_CHANGED, ({ key, val }) => { if (key === 'autoQuiz') { if (val && !_active) start(); else if (!val && _active) stop(); } }); EventBus.on(EventBus.PAGE_CHANGED, () => { if (_active) { _solvedCount = 0; _totalCount = 0; _skippedCount = 0; _solving = false; setTimeout(() => { _solveByPageType(); }, 2000); } }); return { start, stop, isActive }; })(); // ============================================================ // Main - 主入口 // ============================================================ /** * 检测当前页面是否为学习通平台 * @returns {boolean} */ function isChaoxingPlatform() { const host = location.hostname; return /(chaoxing|xuexitong)\.com$/i.test(host) || /\.edu\.cn$/i.test(host); } /** * 路由识别:识别当前页面基础类型 * 注意:课程页面('course')内嵌视频和章节测验两个标签, * QuizSolver 会通过 MutationObserver 自动检测测验DOM的出现。 * @returns {'course'|'media'|'login'|'stuActive'|'cxWork'|'cxExam'|'other'} */ function detectPageType() { const href = location.href; // 随堂练习(iframe 内 answerQuestion2,Vue 渲染) if (/answerQuestion2/i.test(href)) return 'stuActive'; // 新版考试 if (/\/exam-ans\//i.test(href)) return 'cxExam'; // 旧版考试 if (/\/exam\//i.test(href) || /test\//i.test(href)) return 'cxExam'; // 新版作业/章节测试 if (/\/mooc2\/work\//i.test(href) || /\/work\/dowork/i.test(href) || /\/work\/doTest/i.test(href)) return 'cxWork'; // 普通作业 if (/\/work\//i.test(href) || /\/homework\//i.test(href) || /\/quiz\//i.test(href)) return 'cxWork'; // 课程/媒体页面 if (/\/mycourse\//i.test(href) || /\/visit\/.*courseid/i.test(href)) return 'course'; if (/\/login\//i.test(href) || /passport2/i.test(href)) return 'login'; if (/\/knowledge\/card\//i.test(href) || /\/mp4\/.*video/i.test(href) || /\/ananas\//i.test(href)) return 'media'; return 'other'; } /** * 监听 SPA 页面路由变化(learning通使用 pushState 进行页内导航) */ function _watchPageNavigation() { let _lastHref = location.href; // 劫持 history.pushState const origPushState = history.pushState; history.pushState = function (...args) { origPushState.apply(this, args); _onUrlChange(); }; // 劫持 history.replaceState const origReplaceState = history.replaceState; history.replaceState = function (...args) { origReplaceState.apply(this, args); _onUrlChange(); }; // 监听 popstate window.addEventListener('popstate', () => { setTimeout(_onUrlChange, 100); }); // 兜底:每2秒轮询检测URL变化 setInterval(() => { if (location.href !== _lastHref) { _onUrlChange(); } }, 2000); function _onUrlChange() { if (location.href === _lastHref) return; _lastHref = location.href; const newType = detectPageType(); Logger.info(`页面切换: ${newType}`); // 根据新页面类型启停模块 if ((newType === 'course' || newType === 'media') && ConfigStore.get('autoPlay') && !CoursePlayer.isActive()) { CoursePlayer.start(); } if ((newType === 'course' || newType === 'stuActive' || newType === 'cxWork' || newType === 'cxExam') && ConfigStore.get('autoQuiz') && !QuizSolver.isActive()) { QuizSolver.start(); } EventBus.emit(EventBus.PAGE_CHANGED, { type: newType, href: location.href }); } } /** * 初始化脚本 */ function init() { if (!isChaoxingPlatform()) { console.log(`[${SCRIPT_NAME}] 非学习通平台,跳过初始化`); return; } Logger.info(`${SCRIPT_NAME} v${SCRIPT_VERSION} 已加载`); const pageType = detectPageType(); const isInIframe = window.self !== window.top; Logger.info(`当前页面类型: ${pageType}${isInIframe ? ' (iframe)' : ''}`); // 答题 iframe 内只启动答题,不创建面板/反作弊 if (isInIframe) { if (ConfigStore.get('autoQuiz')) { QuizSolver.start(); } return; } // ====== 以下仅顶层窗口执行 ====== // 初始化控制面板 PanelMgr.init(); // 启动反作弊引擎 if (ConfigStore.get('autoPlay') || ConfigStore.get('autoQuiz')) { AntiCheat.enable(); } // 课程/媒体页面 → 启动刷课 + 答题(视频和章节测验在同一页面标签切换) if ((pageType === 'course' || pageType === 'media') && ConfigStore.get('autoPlay')) { CoursePlayer.start(); } if ((pageType === 'course' || pageType === 'stuActive' || pageType === 'cxWork' || pageType === 'cxExam') && ConfigStore.get('autoQuiz')) { QuizSolver.start(); } // SPA 路由变化检测(学习通用 history.pushState 导航) _watchPageNavigation(); console.log(`[${SCRIPT_NAME}] 核心模块初始化完成`); } // ---------- DOM Ready 后启动 ---------- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // 暴露调试接口到全局(开发期使用) if (typeof unsafeWindow !== 'undefined') { unsafeWindow.__AIRE_HELPER__ = { EventBus, ConfigStore, Logger, PanelMgr, AntiCheat, CoursePlayer, AnswerSearcher, AISolver, QuizSolver, version: SCRIPT_VERSION, detectPageType, isChaoxingPlatform }; } })();