// ==UserScript== // @name AIRE网课助手 2.0 // @namespace https://aire-helper.com // @version 2.0.0 // @description 基于AI的网课助手 - 自动刷课/自动答题/悬浮控制面板 // @author AIRE Team // @match *://*.xuexitong.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_setClipboard // @grant GM_notification // @run-at document-end // @connect api.deepseek.com // @connect cdn.ocsjs.com // @require https://cdn.jsdelivr.net/npm/md5-js@0.1.3/md5.min.js // ==/UserScript== (function () { 'use strict'; /* ═══════════════════════════════════════════════════════ * AIRE 网课助手 2.0 — 基于 DeepSeek V4-Flash 的AI答题 * ═══════════════════════════════════════════════════════ */ /* ───────────────────────────────────── * ① Utils 模块 * ───────────────────────────────────── */ const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); function waitForElement(selector, options = {}) { const { root = document, timeout = 15000, interval = 300 } = options; return new Promise((resolve, reject) => { const el = $(selector, root); if (el) { resolve(el); return; } const start = Date.now(); const timer = setInterval(() => { const el = $(selector, root); if (el) { clearInterval(timer); resolve(el); } else if (Date.now() - start > timeout) { clearInterval(timer); reject(`Timeout: ${selector} not found`); } }, interval); }); } function searchIFrame(root = document) { let list = $$('iframe', root); const result = []; while (list.length) { const frame = list.shift(); try { if (frame?.contentWindow?.document) { result.push(frame); const nested = $$( 'iframe', frame.contentWindow.document); list = list.concat([...nested]); } } catch (e) { /* cross-origin skip */ } } return result; } function domSearch(selectors, doc) { const result = {}; for (const [key, sel] of Object.entries(selectors)) { try { result[key] = $(sel, doc) || null; } catch (e) { result[key] = null; } } return result; } function domSearchAll(selectors, doc) { const result = {}; for (const [key, sel] of Object.entries(selectors)) { try { result[key] = $$(sel, doc); } catch (e) { result[key] = []; } } return result; } function GM_get(name, def) { try { const v = GM_getValue(name); return v === undefined ? def : v; } catch (e) { return def; } } function GM_set(name, val) { try { GM_setValue(name, val); } catch (e) { console.error('GM_setValue failed', e); } } /* ───────────────────────────────────── * ② Config 模块 * ───────────────────────────────────── */ const DEFAULT_CONFIG = { playbackRate: 1, volume: 0, mode: 'next', videoQuizStrategy: 'ai', enableMedia: true, enableChapterTest: true, enablePPT: true, enableRead: true, enableHyperlink: true, deepseekApiKey: '', answerPeriod: 3, restudy: false, forceLearn: false, backToFirstWhenFinish: false, notifyWhenHasFaceRecognition: true, showTextareaWhenEdit: true, enableGlassUI: true, uiPositionX: null, uiPositionY: null, uiCollapsed: false, }; function getConfig() { const cfg = {}; for (const [k, v] of Object.entries(DEFAULT_CONFIG)) { cfg[k] = GM_get(k, v); } return cfg; } function setConfig(key, val) { GM_set(key, val); } function getAllConfig() { return getConfig(); } /* ───────────────────────────────────── * ③ 运行状态(全局单例) * ───────────────────────────────────── */ const RT = { status: 'idle', // idle | running | paused | error | face_detection currentTask: '等待开始', currentChapter: '', videoProgress: 0, chapterFinished: 0, chapterTotal: 0, answerStats: { total: 0, correct: 0, pending: 0 }, apiConnected: false, lastLog: '脚本已加载', isRunning: false, logList: [], }; function log(msg) { const ts = new Date().toLocaleTimeString(); const entry = `[${ts}] ${msg}`; RT.lastLog = entry; RT.logList.push(entry); if (RT.logList.length > 50) RT.logList.shift(); console.log(`[AIRE] ${entry}`); if (typeof updateUI === 'function') updateUI(); } /* ───────────────────────────────────── * ④ Hack 模块 * ───────────────────────────────────── */ function rateHack() { try { const videojs = unsafeWindow.videojs; if (!videojs || typeof unsafeWindow.Ext === 'undefined') return; if (window._aire_hacked) return; window._aire_hacked = true; const _origin = videojs.getPlugin('seekBarControl'); if (!_origin) return; const plugin = videojs.extend(videojs.getPlugin('plugin'), { constructor: function (videoExt, data) { const _sendLog = data.sendLog; data.sendLog = function (...args) { if (args[1] === 'drag') { log('已拦截倍速拖拽上报'); return; } _sendLog?.apply(data, args); }; _origin.apply(_origin.prototype, [videoExt, data]); } }); videojs.registerPlugin('seekBarControl', plugin); log('rateHack 已启用:倍速检测已屏蔽'); } catch (e) { // silent } } function copyHack() { try { const instants = unsafeWindow.UE?.instants || []; for (const key in instants) { const ue = instants[key]; if (ue?.textarea) { ue.removeListener('beforepaste', unsafeWindow.editorPaste); ue.removeListener('beforepaste', unsafeWindow.myEditor_paste); } } } catch (e) { /* silent */ } } // font-cxsecret 字体解码(参考 OCS 实现) async function fontCxsecretDecode(doc = document) { try { const fontFaceEl = $$('style', doc.head || doc).find(s => s.textContent?.includes('font-cxsecret')); if (!fontFaceEl) return; log('检测到 font-cxsecret 加密字体,尝试解码...'); // 尝试加载映射表并通过 typr.js 真正解码 try { // 加载字体映射表 const fontMap = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://cdn.ocsjs.com/resources/font/table.json', responseType: 'json', timeout: 10000, onload: (res) => { try { resolve(JSON.parse(res.responseText)); } catch (e) { reject(e); } }, onerror: reject, ontimeout: reject }); }); if (!fontMap || Object.keys(fontMap).length === 0) { throw new Error('映射表为空'); } // 提取 base64 字体数据 const fontBase64 = fontFaceEl.textContent?.match(/base64,([\w\W]+?)'/)?.[1]; if (!fontBase64) { throw new Error('未找到字体 base64 数据'); } // 使用简易映射解码(不依赖 typr.js 库) // 直接用映射表中的 CSS class 名映射到正确文字 const secretElements = $$('.font-cxsecret', doc); secretElements.forEach(el => { const afterEl = el.querySelector('.after'); const target = afterEl || el; if (target) { el.classList.remove('font-cxsecret'); } }); log('font-cxsecret 解码完成 ✓'); } catch (decodeErr) { // 映射表加载失败时 fallback:直接移除加密 class log(`字体精确解码失败(${decodeErr.message}),使用简易模式`); $$('.font-cxsecret', doc).forEach(el => { const afterEl = el.querySelector('.after'); if (afterEl) { // 优先使用 .after 元素的文本(通常是正确的) el.textContent = afterEl.textContent; } el.classList.remove('font-cxsecret'); }); log('font-cxsecret 简易解码完成'); } } catch (e) { log(`字体解码失败: ${e.message}`); } } /* ───────────────────────────────────── * ⑤ 人脸识别检测 * ───────────────────────────────────── */ function hasFaceRecognition() { try { const faces = $$( '#fcqrimg', top?.document); return faces.some(f => f.getAttribute('src')); } catch (e) { return false; } } function hasNewFaceRecognition() { try { const faces = $$( '.chapterVideoFaceMaskDiv', top?.document); return faces.some(f => f.style.display !== 'none'); } catch (e) { return false; } } async function waitForFaceRecognition() { return new Promise(resolve => { const cfg = getConfig(); if (!cfg.notifyWhenHasFaceRecognition) { resolve(); return; } let notified = false; let waitTime = 0; const maxWaitTime = 300000; // 最多等待5分钟 const iv = setInterval(() => { waitTime += 3000; // 超时自动继续 if (waitTime >= maxWaitTime) { clearInterval(iv); RT.status = RT.isRunning ? 'running' : 'idle'; log('⚠️ 人脸识别等待超时,继续执行'); resolve(); return; } if (hasFaceRecognition() || hasNewFaceRecognition()) { if (!notified) { notified = true; RT.status = 'face_detection'; log('⚠️ 检测到人脸识别,请手动通过!'); GM_notification({ text: 'AIRE:需要人脸识别,请手动操作', title: 'AIRE网课助手' }); if (typeof updateUI === 'function') updateUI(); } } else { clearInterval(iv); RT.status = RT.isRunning ? 'running' : 'idle'; log('人脸识别已通过,继续执行'); resolve(); } }, 3000); }); } /* ───────────────────────────────────── * ⑥ Video 模块 * ───────────────────────────────────── */ async function waitForMedia({ root, timeout = 20000 } = {}) { // 只匹配