// ==UserScript== // @name 北京执业药师-金航联平台 (Pro) // @namespace http://tampermonkey.net/ // @version 2.0.1 // @description 金航联平台自动化刷课脚本 v2.0 — 模块化重构,增强性能/可靠性/UI // @author Coren + fix // @match *://bjzyys.mtnet.com.cn/* // @run-at document-start // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_setClipboard // @connect api.deepseek.com // @license CC BY-NC-SA 4.0 // ==/UserScript== (function () { 'use strict'; // ========================================================================= // CONFIG // ========================================================================= const CFG = { MODELS: { flash: 'deepseek-v4-flash', chat: 'deepseek-chat' }, DEFAULT_MODEL: 'flash', API_URL: 'https://api.deepseek.com/chat/completions', API_PROMPT: '你是一个乐于助人的问题回答助手。聚焦于执业药师相关的内容,请根据用户提出的问题,提供准确、清晰的解答。注意回答时仅仅包括答案,不允许其他额外任何解释,输出为一行一道题目的答案,答案只能是题目序号:字母选项,不能包含文字内容。单选输出示例:1.A。多选输出示例:1.ABC。', KEEPALIVE_INTERVAL: 500, URL_CHECK_INTERVAL: 1500, MUTE_SYNC_INTERVAL: 2000, CHAPTER_CHECK_INTERVAL: 5000, API_TIMEOUT: 30000, API_RETRY: 3, API_RETRY_DELAY: 1000, }; // ========================================================================= // STATE // ========================================================================= const S = { get serviceActive() { return GM_getValue('mtnet_service_active', true); }, set serviceActive(v) { GM_setValue('mtnet_service_active', v); }, get muted() { return GM_getValue('mtnet_is_muted', true); }, set muted(v) { GM_setValue('mtnet_is_muted', v); }, get apiKey() { return GM_getValue('deepseek_api_key', ''); }, set apiKey(v) { GM_setValue('deepseek_api_key', v); }, get autoExam() { return GM_getValue('mtnet_auto_exam_mode', false); }, set autoExam(v) { GM_setValue('mtnet_auto_exam_mode', v); }, get model() { return GM_getValue('mtnet_model', CFG.DEFAULT_MODEL); }, set model(v) { GM_setValue('mtnet_model', v); }, get panelCollapsed() { return GM_getValue('mtnet_panel_collapsed', false); }, set panelCollapsed(v) { GM_setValue('mtnet_panel_collapsed', v); }, get examHistory() { return JSON.parse(GM_getValue('mtnet_exam_history', '[]')); }, set examHistory(v) { GM_setValue('mtnet_exam_history', JSON.stringify(v)); }, get logLevel() { return GM_getValue('mtnet_log_level', 1); }, set logLevel(v) { GM_getValue('mtnet_log_level', v); }, // runtime videoRef: null, keepAliveTimer: null, chapterTimer: null, muteTimer: null, urlCheckTimer: null, panelCreated: false, examPanelCreated: false, }; // ========================================================================= // LOGGER // ========================================================================= const LOG = { _fmt(level, tag, msg) { return `[${new Date().toLocaleTimeString()}] [${level}] [${tag}] ${msg}`; }, debug(tag, msg, ...a) { if (S.logLevel >= 0) console.log(this._fmt('DBG', tag, msg), ...a); }, info(tag, msg, ...a) { if (S.logLevel >= 1) console.log(this._fmt('INF', tag, msg), ...a); }, warn(tag, msg, ...a) { if (S.logLevel >= 2) console.warn(this._fmt('WRN', tag, msg), ...a); }, error(tag, msg, ...a) { if (S.logLevel >= 3) console.error(this._fmt('ERR', tag, msg), ...a); }, }; // ========================================================================= // UTILS // ========================================================================= const $ = (sel, root) => (root || document).querySelector(sel); const $$ = (sel, root) => [...(root || document).querySelectorAll(sel)]; const delay = (ms) => new Promise((r) => setTimeout(r, ms)); const origin = () => window.location.origin; function findElementByText(selector, text) { return $$(selector).find((el) => el.innerText?.trim().includes(text.trim())); } function clickEl(el, desc) { if (!el || typeof el.click !== 'function') { LOG.warn('util', `点击失败: ${desc}`); return false; } LOG.debug('util', `点击: ${desc}`); el.click(); return true; } function clickElEnhanced(el, desc) { if (!el) { LOG.warn('util', `增强点击失败: ${desc}`); return false; } LOG.debug('util', `增强点击: ${desc}`); const input = $('input[type="radio"], input[type="checkbox"]', el); if (input) { input.click(); return true; } el.click(); try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } catch {} return true; } // ========================================================================= // ANTI-DETECTION (must run at document-start) // ========================================================================= const AntiDetect = { init() { this._hijackVisibility(); this._hijackHasFocus(); this._interceptEvents(); this._interceptPause(); this._setupFocusRestore(); LOG.info('anti', '防检测机制已初始化'); }, _hijackVisibility() { const def = (obj, prop, val) => { try { Object.defineProperty(obj, prop, { get: () => val, configurable: false }); } catch { try { Object.defineProperty(obj, prop, { get: () => val, configurable: true }); } catch (e) { LOG.error('anti', `hijack ${prop} 失败`, e); } } }; def(document, 'visibilityState', 'visible'); def(document, 'hidden', false); }, _hijackHasFocus() { try { Document.prototype.hasFocus = () => true; } catch (e) { LOG.error('anti', 'hijack hasFocus 失败', e); } }, _interceptEvents() { document.addEventListener('visibilitychange', (e) => e.stopImmediatePropagation(), true); window.addEventListener('blur', (e) => e.stopImmediatePropagation(), true); }, _interceptPause() { document.addEventListener('pause', (e) => { if (!(e.target instanceof HTMLVideoElement)) return; e.stopImmediatePropagation(); if (!e.target.ended) e.target.play().catch(() => {}); }, true); }, _setupFocusRestore() { window.addEventListener('focus', () => { for (const v of $$('video')) { if (v.paused && !v.ended) v.play().catch(() => {}); } }); }, }; // ========================================================================= // VIDEO CONTROLLER // ========================================================================= const VideoCtrl = { _cachedRef: null, start() { KeepAlive.start(); MuteSync.start(); this._observeDOM(); }, stop() { KeepAlive.stop(); MuteSync.stop(); }, async find(timeout = 15000) { if (this._cachedRef && !this._cachedRef.isConnected) this._cachedRef = null; if (this._cachedRef) return this._cachedRef; const t0 = Date.now(); while (Date.now() - t0 < timeout) { const v = $('video'); if (v) { this._cachedRef = v; return v; } const iframe = $('iframe'); if (iframe?.contentWindow) { try { const iv = iframe.contentWindow.document.querySelector('video'); if (iv) { this._cachedRef = iv; return iv; } } catch {} } await delay(500); } return null; }, _observeDOM() { const obs = new MutationObserver(() => { this._cachedRef = null; }); obs.observe(document.body || document.documentElement, { childList: true, subtree: true }); }, }; // ========================================================================= // KEEP-ALIVE // ========================================================================= const KeepAlive = { start() { if (S.keepAliveTimer) return; S.keepAliveTimer = setInterval(() => { for (const v of $$('video')) { if (v.paused && !v.ended && v.readyState >= 2) v.play().catch(() => {}); } }, CFG.KEEPALIVE_INTERVAL); }, stop() { if (S.keepAliveTimer) { clearInterval(S.keepAliveTimer); S.keepAliveTimer = null; } }, }; // ========================================================================= // MUTE SYNC // ========================================================================= const MuteSync = { start() { this._tick(); }, stop() { if (S.muteTimer) { clearTimeout(S.muteTimer); S.muteTimer = null; } }, async _tick() { if (!S.serviceActive || !window.location.href.includes('/video/')) { S.muteTimer = setTimeout(() => this._tick(), CFG.MUTE_SYNC_INTERVAL); return; } try { const v = await VideoCtrl.find(); if (v && v.muted !== S.muted) v.muted = S.muted; } catch (e) { LOG.warn('mute', '同步异常', e); } S.muteTimer = setTimeout(() => this._tick(), CFG.MUTE_SYNC_INTERVAL); }, }; // ========================================================================= // API CLIENT // ========================================================================= const Api = { async ask(questions, modelKey) { const model = CFG.MODELS[modelKey || S.model] || CFG.MODELS.flash; let lastErr; for (let i = 0; i < CFG.API_RETRY; i++) { try { const result = await this._request(model, questions); return result; } catch (e) { lastErr = e; LOG.warn('api', `请求失败 (${i + 1}/${CFG.API_RETRY}): ${e.message}`); if (i < CFG.API_RETRY - 1) await delay(CFG.API_RETRY_DELAY * (i + 1)); } } throw lastErr; }, _request(model, content) { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('API 请求超时')), CFG.API_TIMEOUT); GM_xmlhttpRequest({ method: 'POST', url: CFG.API_URL, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${S.apiKey}` }, data: JSON.stringify({ model, messages: [ { role: 'system', content: CFG.API_PROMPT }, { role: 'user', content }, ], stream: false, thinking: { type: 'disabled' }, }), onload(res) { clearTimeout(timer); if (res.status === 200) { try { const r = JSON.parse(res.responseText); resolve(r.choices[0].message.content); } catch (e) { reject(new Error('解析响应失败')); } } else { reject(new Error(`API 状态码: ${res.status}`)); } }, onerror(e) { clearTimeout(timer); reject(new Error('网络请求错误')); }, }); }); }, }; // ========================================================================= // EXAM ENGINE // ========================================================================= const ExamEngine = { extract() { const qs = $$('div[id^="q"]'); if (!qs.length) { LOG.error('exam', '未找到题目容器'); return null; } let text = ''; let n = 1; for (const q of qs) { const allText = q.innerText; const opts = $$('.el-radio__label, .el-checkbox__label', q); let end = allText.length; for (const o of opts) { const idx = allText.indexOf(o.innerText); if (idx !== -1 && idx < end) end = idx; } const stem = allText.substring(0, end).trim().replace(/^\d+\.?\s*/, ''); if (!stem) continue; text += `${n}. ${stem}\n`; for (const o of opts) text += o.innerText + '\n'; text += '\n'; n++; } LOG.info('exam', `提取 ${n - 1} 道题`); return text || null; }, parseAnswers(answerText) { const map = new Map(); answerText.trim().split('\n').forEach((line) => { const m = line.match(/(\d+)\s*\.\s*([A-Z]+)/i); if (m) map.set(parseInt(m[1], 10), m[2].toUpperCase()); }); return map; }, async fill(answers) { const map = typeof answers === 'string' ? this.parseAnswers(answers) : answers; const qs = $$('div[id^="q"]'); for (let i = 0; i < qs.length; i++) { const q = qs[i]; const num = i + 1; if (!map.has(num)) { LOG.warn('exam', `第 ${num} 题无答案`); continue; } const chars = map.get(num); LOG.debug('exam', `填写第 ${num} 题: ${chars}`); for (const ch of chars.split('')) { const labels = $$('.el-radio, .el-checkbox', q); let ok = false; for (const lb of labels) { const lbText = $('.el-radio__label, .el-checkbox__label', lb)?.innerText?.trim(); if (lbText?.startsWith(ch)) { clickElEnhanced(lb, `Q${num} 选项 ${ch}`); await delay(200); ok = true; } } if (!ok) LOG.warn('exam', `Q${num} 未找到选项 ${ch}`); } } }, findSubmitBtn() { for (const kw of ['交卷', '提交试卷', '提交']) { const btn = findElementByText('span, button, a, div, .btn, .el-button', kw); if (btn) return btn; } return $('.nextClick, .nextBtn, .loginBtn, .btn-submit, .submit-btn, .el-button--primary, button[type="submit"]'); }, async confirmDialog() { for (let i = 0; i < 40; i++) { await delay(250); for (const sel of [ '.el-message-box__btns .el-button--primary', '.el-dialog__footer .el-button--primary', '.el-message-box__btns button', '.el-dialog__footer button', ]) { for (const btn of $$(sel)) { const t = btn.innerText?.trim(); if (['确定', '确认', '确认交卷', '是', 'OK'].includes(t)) { LOG.info('exam', `确认按钮: ${t}`); btn.click(); return true; } } } for (const kw of ['确定', '确认', '确认交卷']) { const btn = findElementByText('button, .el-button, .btn', kw); if (btn?.closest('.el-message-box, .el-dialog, .modal, .popup, .el-message')) { btn.click(); return true; } } } LOG.warn('exam', '未找到确认对话框'); return false; }, saveHistory(text, answers) { const h = S.examHistory; h.push({ time: new Date().toISOString(), url: location.href, questions: text, answers }); if (h.length > 50) h.splice(0, h.length - 50); S.examHistory = h; }, }; // ========================================================================= // NOTIFICATION // ========================================================================= const Notify = { show(title, text, onclick) { try { GM_notification({ title, text, timeout: 5000, onclick }); } catch {} }, }; // ========================================================================= // UI — CONTROL PANEL // ========================================================================= const UI = { create() { if (S.panelCreated) return; S.panelCreated = true; this._injectStyles(); const panel = this._buildPanel(); document.body.appendChild(panel); requestAnimationFrame(() => { const w = panel.offsetWidth || 240; const h = panel.offsetHeight || 260; panel.style.left = Math.max(0, window.innerWidth - w - 20) + 'px'; panel.style.top = Math.max(0, window.innerHeight - h - 20) + 'px'; }); this._bindEvents(panel); Draggable.init(panel, $('#control-panel-header', panel)); }, _injectStyles() { GM_addStyle(` #control-panel{position:fixed;width:240px;background:#fff;border:1px solid #e0e0e0;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,.15);z-index:10000;font-family:'Microsoft YaHei',sans-serif;transition:height .25s,box-shadow .25s;overflow:hidden} #control-panel:hover{box-shadow:0 6px 28px rgba(0,0,0,.2)} #control-panel-header{padding:8px 12px;background:linear-gradient(135deg,#007bff,#0056b3);color:#fff;cursor:move;user-select:none;font-size:13px;display:flex;align-items:center;justify-content:space-between} #control-panel-header .panel-title{font-weight:600;letter-spacing:.5px} .panel-header-btns{display:flex;gap:4px} .panel-header-btns button{background:rgba(255,255,255,.2);border:none;color:#fff;width:24px;height:24px;border-radius:4px;cursor:pointer;font-size:14px;line-height:24px;text-align:center;padding:0;transition:background .2s} .panel-header-btns button:hover{background:rgba(255,255,255,.35)} #control-panel-content{padding:12px;display:flex;flex-direction:column;gap:8px;transition:max-height .25s,opacity .2s,padding .25s;overflow:hidden} #control-panel-content.collapsed{max-height:0!important;padding:0 12px!important;opacity:0;pointer-events:none} .panel-btn{padding:7px 14px;font-size:13px;color:#fff;border:none;border-radius:6px;cursor:pointer;transition:all .2s;width:100%;box-sizing:border-box;font-weight:500} .panel-btn:hover{filter:brightness(1.1);transform:translateY(-1px)} .panel-btn:active{transform:translateY(0)} .service-btn-active{background:linear-gradient(135deg,#28a745,#1e7e34)} .service-btn-paused{background:linear-gradient(135deg,#dc3545,#b02a37)} .mute-btn-on{background:linear-gradient(135deg,#ffc107,#e0a800);color:#333} .mute-btn-off{background:linear-gradient(135deg,#6c757d,#5a6268)} .panel-divider{width:100%;height:1px;background:#eee;margin:2px 0} .setting-row{display:flex;flex-direction:column;width:100%;gap:4px} .setting-row>label{font-size:12px;font-weight:600;color:#666} #api-key-input{width:100%;padding:6px 8px;border-radius:6px;border:1px solid #ddd;box-sizing:border-box;font-size:12px;transition:border-color .2s} #api-key-input:focus{border-color:#007bff;outline:none;box-shadow:0 0 0 2px rgba(0,123,255,.15)} .model-select{width:100%;padding:5px 8px;border-radius:6px;border:1px solid #ddd;box-sizing:border-box;font-size:12px} .config-btns{display:flex;gap:4px} .config-btns button{flex:1;padding:5px;font-size:11px;border:1px solid #ddd;border-radius:4px;cursor:pointer;background:#f8f9fa} .config-btns button:hover{background:#e9ecef} .log-level-row{display:flex;align-items:center;gap:6px} .log-level-row label{font-size:11px;color:#999} .log-level-row select{font-size:11px;padding:2px 4px;border-radius:4px;border:1px solid #ddd} `); }, _buildPanel() { const collapsed = S.panelCollapsed; const div = document.createElement('div'); div.id = 'control-panel'; div.innerHTML = `
⚡ 刷课助手 v2.0
`; return div; }, _bindEvents(panel) { const $id = (id) => panel.querySelector(`#${id}`); // API key (不通过 innerHTML,防 XSS) const keyInput = $id('api-key-input'); keyInput.value = S.apiKey; keyInput.addEventListener('change', () => { S.apiKey = keyInput.value.trim(); }); // model const modelSel = $id('model-select'); modelSel.value = S.model; modelSel.addEventListener('change', () => { S.model = modelSel.value; }); // log level const logSel = $id('log-level-select'); logSel.value = String(S.logLevel); logSel.addEventListener('change', () => { S.logLevel = parseInt(logSel.value); }); // collapse const content = $id('control-panel-content'); const collapseBtn = $id('collapse-btn'); collapseBtn.addEventListener('click', () => { const c = content.classList.toggle('collapsed'); collapseBtn.textContent = c ? '▸' : '▾'; S.panelCollapsed = c; }); // minimize (收起为小图标) $id('minimize-btn').addEventListener('click', () => { panel.style.display = 'none'; const badge = document.createElement('div'); badge.id = 'control-panel-badge'; badge.textContent = '⚡'; badge.style.cssText = 'position:fixed;bottom:20px;right:20px;width:40px;height:40px;background:linear-gradient(135deg,#007bff,#0056b3);color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:10000;font-size:18px;box-shadow:0 2px 10px rgba(0,0,0,.3)'; badge.addEventListener('click', () => { badge.remove(); panel.style.display = ''; }); document.body.appendChild(badge); }); // service toggle const svcBtn = $id('service-toggle-btn'); const updateSvc = (a) => { svcBtn.textContent = a ? '● 运行中' : '● 已暂停'; svcBtn.className = 'panel-btn ' + (a ? 'service-btn-active' : 'service-btn-paused'); }; updateSvc(S.serviceActive); svcBtn.addEventListener('click', () => { S.serviceActive = !S.serviceActive; location.reload(); }); // mute toggle const muteBtn = $id('mute-toggle-btn'); const updateMute = (m) => { muteBtn.textContent = m ? '🔇 静音' : '🔊 有声'; muteBtn.className = 'panel-btn ' + (m ? 'mute-btn-on' : 'mute-btn-off'); }; updateMute(S.muted); muteBtn.addEventListener('click', async () => { S.muted = !S.muted; updateMute(S.muted); const v = await VideoCtrl.find(); if (v) v.muted = S.muted; }); // export (不导出 apiKey,防止泄露) $id('export-config-btn').addEventListener('click', () => { const cfg = { model: S.model, muted: S.muted, autoExam: S.autoExam, logLevel: S.logLevel, examHistory: S.examHistory, }; const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `mtnet-config-${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(a.href); Notify.show('导出成功', '配置已导出(API Key 未包含)'); }); // import (导入配置,apiKey 字段会被忽略,需手动输入) $id('import-config-btn').addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const cfg = JSON.parse(reader.result); if (cfg.apiKey) { Notify.show('提示', '导入文件包含 API Key,出于安全考虑已忽略,请手动输入'); } if (cfg.model) S.model = cfg.model; if (cfg.muted !== undefined) S.muted = cfg.muted; if (cfg.autoExam !== undefined) S.autoExam = cfg.autoExam; if (cfg.logLevel !== undefined) S.logLevel = cfg.logLevel; if (cfg.examHistory) S.examHistory = cfg.examHistory; location.reload(); } catch { Notify.show('导入失败', '文件格式错误'); } }; reader.readAsText(file); }); input.click(); }); // history $id('view-history-btn').addEventListener('click', () => { const h = S.examHistory; if (!h.length) { Notify.show('答题历史', '暂无记录'); return; } const text = h.map((e, i) => `#${i + 1} ${e.time}\n${e.answers || '(无答案)'}`).join('\n\n'); GM_setClipboard(text); Notify.show('已复制', `${h.length} 条历史已复制到剪贴板`); }); }, }; // ========================================================================= // UI — EXAM PANEL // ========================================================================= const ExamUI = { create() { if (S.examPanelCreated) return; S.examPanelCreated = true; this._injectStyles(); const panel = this._build(); document.body.appendChild(panel); this._bind(panel); Draggable.init(panel, $('#exam-helper-header', panel)); }, destroy() { const p = $('#exam-helper-panel'); if (p) p.remove(); S.examPanelCreated = false; }, _injectStyles() { GM_addStyle(` #exam-helper-panel{position:fixed;top:20px;left:20px;width:450px;background:#f9f9f9;border:1px solid #007bff;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.2);z-index:10001;font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;max-height:90vh} #exam-helper-header{padding:10px 15px;background-color:#007bff;color:#fff;cursor:move;border-top-left-radius:7px;border-top-right-radius:7px;font-size:16px;flex-shrink:0;display:flex;justify-content:space-between;align-items:center} #exam-helper-content{padding:15px;display:flex;flex-direction:column;gap:15px;overflow-y:auto} #exam-action-btn{color:#fff;padding:10px;border:none;border-radius:5px;font-size:16px;cursor:pointer;transition:background-color .3s} #exam-action-btn:disabled{background-color:#ccc;cursor:not-allowed} #exam-status{margin-top:5px;padding:10px;background-color:#e9ecef;border-radius:5px;text-align:center;font-size:14px;color:#495057;min-height:20px} .auto-mode-switch{display:flex;align-items:center;gap:8px;background:#e9ecef;padding:8px;border-radius:5px} .display-area{border:1px solid #ddd;background:#fff;padding:10px;border-radius:5px;max-height:200px;overflow-y:auto} .display-area h4{margin:0 0 10px 0;font-size:14px;border-bottom:1px solid #eee;padding-bottom:5px} .display-area pre{white-space:pre-wrap;word-wrap:break-word;font-size:12px;margin:0} #manual-ask-container{display:flex;flex-direction:column;gap:10px} #manual-question-input{width:100%;min-height:60px;padding:8px;border-radius:4px;border:1px solid #ccc;box-sizing:border-box} #manual-ask-btn{background-color:#5a6268;color:#fff;padding:8px;border:none;border-radius:5px;cursor:pointer} .exam-progress{width:100%;height:6px;background:#e9ecef;border-radius:3px;overflow:hidden} .exam-progress-bar{height:100%;background:linear-gradient(90deg,#28a745,#20c997);transition:width .3s;width:0%} `); }, _build() { const div = document.createElement('div'); div.id = 'exam-helper-panel'; div.innerHTML = `
AI 答题助手 v4.0
请先在主面板输入 API Key。

提取的题目:

AI 回答:

手动提问区

`; return div; }, _bind(panel) { const $id = (id) => panel.querySelector(`#${id}`); const actionBtn = $id('exam-action-btn'); const status = $id('exam-status'); const autoCheck = $id('auto-mode-checkbox'); const qPre = $id('question-display').querySelector('pre'); const aPre = $id('answer-display').querySelector('pre'); const pBar = $id('exam-progress-bar'); const manualInput = $id('manual-question-input'); const manualBtn = $id('manual-ask-btn'); const manualDiv = $id('manual-answer-display'); const manualPre = manualDiv.querySelector('pre'); autoCheck.checked = S.autoExam; autoCheck.addEventListener('change', () => { S.autoExam = autoCheck.checked; status.textContent = `自动模式${S.autoExam ? '开启' : '关闭'}`; }); const setProgress = (pct) => { pBar.style.width = pct + '%'; }; const setBtn = (text, color, disabled) => { actionBtn.textContent = text; actionBtn.style.backgroundColor = color; actionBtn.disabled = disabled; }; setBtn('开始自动答题', '#28a745', false); // minimize $id('exam-minimize-btn').addEventListener('click', () => { panel.style.display = 'none'; const b = document.createElement('div'); b.id = 'exam-badge'; b.textContent = '📝'; b.style.cssText = 'position:fixed;top:20px;right:20px;width:40px;height:40px;background:#007bff;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:10001;font-size:18px;box-shadow:0 2px 10px rgba(0,0,0,.3)'; b.addEventListener('click', () => { b.remove(); panel.style.display = ''; }); document.body.appendChild(b); }); // manual ask manualBtn.addEventListener('click', async () => { const q = manualInput.value.trim(); if (!q) { alert('请输入问题!'); return; } if (!S.apiKey) { alert('请输入 API Key!'); return; } manualBtn.disabled = true; manualBtn.textContent = '思考中...'; manualDiv.style.display = 'block'; manualPre.textContent = '...'; try { manualPre.textContent = await Api.ask(q); } catch (e) { manualPre.textContent = `失败: ${e.message}`; } finally { manualBtn.disabled = false; manualBtn.textContent = '手动提问'; } }); // auto run const runAuto = async () => { try { setBtn('提取题目...', '#ffc107', true); setProgress(10); status.textContent = '正在提取题目...'; const text = ExamEngine.extract(); if (!text) throw new Error('未提取到题目'); qPre.textContent = text; setBtn('请求 AI...', '#ffc107', true); setProgress(30); status.textContent = '正在请求 AI...'; const answers = await Api.ask(text); if (!answers) throw new Error('AI 未返回答案'); aPre.textContent = answers; setBtn('填写答案...', '#ffc107', true); setProgress(60); status.textContent = '正在填写答案...'; await ExamEngine.fill(answers); setProgress(85); status.textContent = '即将交卷...'; await delay(1500); const btn = ExamEngine.findSubmitBtn(); if (!btn) throw new Error('未找到交卷按钮'); clickElEnhanced(btn, '交卷'); status.textContent = '等待确认...'; await ExamEngine.confirmDialog(); ExamEngine.saveHistory(text, answers); setProgress(100); status.textContent = '已交卷!'; Notify.show('考试完成', '已自动交卷'); await delay(3000); location.href = `${origin()}/index`; } catch (e) { LOG.error('exam', '自动模式错误', e); status.textContent = `错误: ${e.message}`; status.style.color = 'red'; setBtn('重新开始', '#28a745', false); setProgress(0); } }; // manual step-by-step let step = 'init'; actionBtn.addEventListener('click', async () => { if (!S.apiKey) { status.textContent = '请先输入 API Key'; status.style.color = 'red'; return; } if (autoCheck.checked) { runAuto(); return; } try { if (step === 'init') { setBtn('提取中...', '#ffc107', true); status.textContent = '提取题目并请求 AI...'; const text = ExamEngine.extract(); if (!text) throw new Error('未提取到题目'); qPre.textContent = text; const answers = await Api.ask(text); if (!answers) throw new Error('AI 未返回答案'); aPre.textContent = answers; step = 'fill'; setBtn('填写答案', '#17a2b8', false); status.textContent = 'AI 分析完成,点击填写'; status.style.color = '#007bff'; } else if (step === 'fill') { setBtn('填写中...', '#ffc107', true); status.textContent = '正在填写...'; await ExamEngine.fill(aPre.textContent); step = 'submit'; setBtn('确认交卷', '#dc3545', false); status.textContent = '已填写,点击交卷'; status.style.color = 'green'; } else if (step === 'submit') { setBtn('交卷中...', '#6c757d', true); const btn = ExamEngine.findSubmitBtn(); if (!btn) throw new Error('未找到交卷按钮'); clickElEnhanced(btn, '交卷'); status.textContent = '已交卷'; await delay(3000); location.href = `${origin()}/index`; } } catch (e) { LOG.error('exam', '手动模式错误', e); status.textContent = `错误: ${e.message}`; status.style.color = 'red'; step = 'init'; setBtn('重新开始', '#28a745', false); } }); }, }; // ========================================================================= // DRAGGABLE // ========================================================================= const Draggable = { init(el, handle) { let dragging = false, ox, oy; handle.addEventListener('mousedown', (e) => { dragging = true; ox = e.clientX - el.getBoundingClientRect().left; oy = e.clientY - el.getBoundingClientRect().top; handle.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!dragging) return; let x = e.clientX - ox; let y = e.clientY - oy; x = Math.max(0, Math.min(x, window.innerWidth - el.offsetWidth)); y = Math.max(0, Math.min(y, window.innerHeight - el.offsetHeight)); el.style.left = x + 'px'; el.style.top = y + 'px'; }); document.addEventListener('mouseup', () => { dragging = false; handle.style.cursor = 'move'; }); }, }; // ========================================================================= // PAGE HANDLERS // ========================================================================= const Pages = { courseList() { LOG.info('page', '课程列表'); try { const target = GM_getValue('mtnet_target_task', 'specialized_video'); const isExam = target.includes('exam'); const courseType = target.includes('specialized') ? '专业科目' : '公需科目'; const filter = isExam ? '待考试' : '未完成'; const btnSel = isExam ? '.indexTextBtn.onlineTest' : '.indexTextBtn'; const btnText = isExam ? '在线考试' : '进入学习'; const tab = findElementByText('span.gxkn', courseType); if (tab && !tab.classList.contains('active')) { clickEl(tab, `${courseType} 标签`); setTimeout(() => {}, 2000); } const flt = findElementByText('a.screenItem', filter); if (flt && !flt.classList.contains('active')) { clickEl(flt, `${filter} 筛选`); setTimeout(() => {}, 2000); } for (const course of $$('.indexCourseListSLi')) { const btn = $(btnSel, course); if (btn?.innerText?.trim() === btnText) { clickEl(btn, btnText); return; } } LOG.info('page', '无可操作课程'); } catch (e) { LOG.error('page', '课程列表处理异常', e); } }, video() { LOG.info('page', '视频页面'); VideoCtrl.start(); if (S.chapterTimer) clearInterval(S.chapterTimer); S.chapterTimer = setInterval(() => { if (!location.href.includes('/video/')) { Pages._leaveVideo(); return; } const prog = $('.courseProgress .gkjd span[style*="color: rgb(255, 148, 102)"]'); if (prog?.innerText?.trim().includes('100')) { Pages._leaveVideo(); location.href = `${origin()}/index`; return; } const chapters = $$('.detailRightC .chapterList li'); const next = chapters.find((ch) => !$('span.chapterPro.floatR.currProgress', ch)?.innerText?.trim().includes('100')); if (next && !next.classList.contains('active')) { clickEl($('a', next), '下一章节'); } else if (!next) { Pages._leaveVideo(); location.href = `${origin()}/index`; } }, CFG.CHAPTER_CHECK_INTERVAL); }, exam() { LOG.info('page', '考试页面'); VideoCtrl.stop(); ExamUI.destroy(); ExamUI.create(); }, _leaveVideo() { if (S.chapterTimer) { clearInterval(S.chapterTimer); S.chapterTimer = null; } VideoCtrl.stop(); }, }; // ========================================================================= // KEYBOARD SHORTCUTS // ========================================================================= const Shortcuts = { init() { document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return; // Ctrl+Shift+S: 暂停/恢复 if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); S.serviceActive = !S.serviceActive; location.reload(); } // Ctrl+Shift+M: 静音切换 if (e.ctrlKey && e.shiftKey && e.key === 'M') { e.preventDefault(); S.muted = !S.muted; Notify.show('静音切换', S.muted ? '已静音' : '已开启声音'); } // Ctrl+Shift+H: 显示/隐藏面板 if (e.ctrlKey && e.shiftKey && e.key === 'H') { e.preventDefault(); const p = $('#control-panel'); if (p) p.style.display = p.style.display === 'none' ? '' : 'none'; } }); }, }; // ========================================================================= // ROUTER & INIT // ========================================================================= const Router = { _lastUrl: '', run() { if (!S.serviceActive) { LOG.info('router', '服务已暂停'); return; } if (S.chapterTimer) { clearInterval(S.chapterTimer); S.chapterTimer = null; } const url = location.href; LOG.info('router', `URL: ${url}`); if (url.includes('/user/courses')) Pages.courseList(); else if (url.includes('/video/')) Pages.video(); else if (url.includes('/examination/')) Pages.exam(); else { LOG.info('router', '未匹配页面'); VideoCtrl.stop(); } }, start() { this.run(); if (S.urlCheckTimer) clearInterval(S.urlCheckTimer); S.urlCheckTimer = setInterval(() => { if (location.href !== this._lastUrl) { this._lastUrl = location.href; this.run(); } }, CFG.URL_CHECK_INTERVAL); }, }; // ========================================================================= // BOOT // ========================================================================= AntiDetect.init(); window.addEventListener('load', () => { LOG.info('boot', 'v2.0.0 启动'); Shortcuts.init(); UI.create(); if (S.serviceActive) { setTimeout(() => Router.start(), 1500); } }); })();