// ==UserScript== // @name 【超星学习通】免费题库+AI答题|视频挂机|章节测验|考试自动作答|字体解密 // @namespace http://tampermonkey.net/ // @icon http://pan-yz.chaoxing.com/favicon.ico // @version 2.0.0 // @description 全自动刷课+答题:视频挂机/章节测验/考试作答。题库命中跳过AI,免费省token // @author 星路遥光 // @license Apache-2.0 // @match *://*.chaoxing.com/* // @match *://*.edu.cn/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_addValueChangeListener // @grant GM_getResourceText // @resource ttf https://www.forestpolice.org/ttf/2.0/table.json // @require https://lib.baomitu.com/blueimp-md5/2.19.0/js/md5.min.js // @connect api.muketool.com // @connect cx.lyck6.cn // @connect apione.apibyte.cn // @connect api.deepseek.com // @connect cx.icodef.com // ==/UserScript== (function () { 'use strict'; // ======================== ====== 全局状态 ============================ let runFlag = true; let answeringSet = new Set(); let isBatchRunning = false; let batchAbort = false; const CACHE_KEY = "ai_answered_v2"; const APIBYTE_DAILY_LIMIT = 50; // apibyte 免费版每日额度上限 let answeredCache = new Set( (GM_getValue(CACHE_KEY, "") || "").split(",").filter(Boolean) ); function persistCache() { GM_setValue(CACHE_KEY, Array.from(answeredCache).join(",")); } // ======================== ====== 配置(首次使用需自行填写) ============================ let config = { apiUrl: GM_getValue('apiUrl', ''), apiKey: GM_getValue('apiKey', ''), model: GM_getValue('model', ''), autoAnswer: GM_getValue('autoAnswer', true), // 刷课模块配置 lyckToken: GM_getValue('lyckToken', ''), videoSpeed: GM_getValue('videoSpeed', 2), autoSubmit: GM_getValue('autoSubmit', true), autoNext: GM_getValue('autoNext', true) }; // ======================== ====== 刷课模块全局状态 ============================ let cxv_isRunning = false; let cxv_missionList = []; let cxv_currentIdx = 0; let cxv_reportInterval = null; let cxw_isProcessing = false; let cxw_pendingWorks = []; let cxn_enabled = true; let cxn_timeoutId = null; // ======================== ====== 样式 ============================ GM_addStyle(` #ai-helper-panel {position:fixed;top:100px;right:20px;width:300px;background:#fff;border:1px solid #ccc;box-shadow:0 4px 12px rgba(0,0,0,0.15);z-index:999999;font-family:sans-serif;border-radius:8px;overflow:hidden} #ai-helper-header {background:#4caf50;color:#fff;padding:10px;cursor:move;font-weight:700;display:flex;justify-content:space-between;align-items:center} #ai-helper-body {padding:15px; max-height:75vh; overflow-y:auto; overflow-x:hidden} #ai-helper-body::-webkit-scrollbar {width:5px} #ai-helper-body::-webkit-scrollbar-thumb {background:#aaa;border-radius:3px} .ai-form-group {margin-bottom:10px} .ai-form-group label {display:block;font-size:13px;margin-bottom:4px;color:#333} .ai-form-group input[type=text],.ai-form-group input[type=password] {width:100%;padding:6px;box-sizing:border-box;border:1px solid #ddd;border-radius:4px} .ai-form-group input[type=checkbox] {margin-right:5px} .ai-btn {background:#4caf50;color:#fff;border:none;padding:8px 12px;width:100%;border-radius:4px;cursor:pointer;font-size:14px;margin-top:10px} .ai-btn:hover {background:#45a049} .btn-stop {background:#f44336} .btn-stop:hover {background:#d32f2f} .btn-clear {background:#9c27b0} .btn-clear:hover {background:#7b1fa2} #ai-status {margin-top:10px;font-size:12px;color:#666;word-break:break-all} #ai-logs {max-height:120px;overflow-y:auto;font-size:12px;margin-top:10px;background:#f9f9f9;border:1px solid #ddd;padding:5px;border-radius:4px;word-break:break-all} .ai-log-item {margin-bottom:4px;border-bottom:1px dashed #eee;padding-bottom:2px} .ai-highlight-answering {outline:3px solid #ff9800!important;outline-offset:2px!important} .ai-highlight-done {outline:2px solid #4caf50!important;outline-offset:1px!important} /* 首次配置提示 */ #ai-first-config-overlay {position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:9999999;display:flex;align-items:center;justify-content:center} #ai-first-config-box {background:#fff;border-radius:12px;padding:30px;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.3);font-family:sans-serif} #ai-first-config-box h2 {margin:0 0 8px;color:#333;font-size:20px} #ai-first-config-box p {color:#666;font-size:14px;margin-bottom:16px;line-height:1.5} #ai-first-config-box .ai-form-group {margin-bottom:12px} #ai-first-config-box input {width:100%;padding:10px;border:1px solid #ddd;border-radius:6px;font-size:14px;box-sizing:border-box} #ai-first-config-box input:focus {border-color:#4caf50;outline:none;box-shadow:0 0 0 2px rgba(76,175,80,0.2)} #ai-first-config-box .ai-btn {padding:10px;font-size:15px} .ai-config-hint {font-size:12px;color:#999;margin-top:3px} /* 刷课模块样式 */ #cxvSpeed {width:100%;margin:5px 0;height:4px;cursor:pointer} #cxvSpeedLabel {font-size:12px;color:#666;min-width:25px;text-align:right} #cxvStatus {font-size:12px;color:#666;margin-top:5px;padding:4px;background:#f5f5f5;border-radius:3px} .ai-section-sep {margin:10px 0;border:none;border-top:1px solid #e0e0e0} .ai-section-title {font-size:13px;font-weight:bold;margin-bottom:5px;color:#333} `); // ======================== ====== 首次配置弹窗 ============================ function showFirstConfig() { if (document.getElementById('ai-first-config-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'ai-first-config-overlay'; overlay.innerHTML = `

🔧 首次使用 · API 配置

请填写你的 AI 接口信息,配置后可随时在侧边面板修改。

支持标准 OpenAI 兼容接口,例如 https://api.deepseek.com/v1
`; document.body.appendChild(overlay); document.getElementById('btnConfigSave').onclick = () => { const url = document.getElementById('cfgUrlFirst').value.trim(); const key = document.getElementById('cfgKeyFirst').value.trim(); const model = document.getElementById('cfgModelFirst').value.trim(); if (!url) return alert('请填写 API 地址'); if (!key) return alert('请填写 API Key'); if (!model) return alert('请填写模型名称'); config.apiUrl = url; config.apiKey = key; config.model = model; Object.entries(config).forEach(([k, v]) => GM_setValue(k, v)); overlay.remove(); addLog('✅ 首次配置完成', 'green'); updateStatus('✅ 已配置,开始使用', 'green'); if (config.autoAnswer) startMonitor(); }; } // ======================== ====== UI ============================ function createUI() { if (document.getElementById('ai-helper-panel')) return; const p = document.createElement('div'); p.id = 'ai-helper-panel'; p.innerHTML = `
🛡 AI答题(强保护版) [-]
状态:缓存${answeredCache.size}题


🎬 刷课模块
${config.videoSpeed}x
刷课: 未启动
`; document.body.appendChild(p); // 事件绑定 const drag = (h) => { let d=!1,sx,sy,ix,iy; h.addEventListener('mousedown',e=>{if(e.target.id==='toggleBtn')return;d=!0;sx=e.clientX;sy=e.clientY;ix=p.offsetLeft;iy=p.offsetTop;document.addEventListener('mousemove',m);document.addEventListener('mouseup',u)}); function m(e){if(!d)return;p.style.left=ix+e.clientX-sx+'px';p.style.top=iy+e.clientY-sy+'px';p.style.right='auto'} function u(){d=!1;document.removeEventListener('mousemove',m);document.removeEventListener('mouseup',u)} }; drag(document.getElementById('ai-helper-header')); document.getElementById('toggleBtn').onclick=()=>{ const b=document.getElementById('ai-helper-body'); b.style.display=b.style.display==='none'?'block':'none'; document.getElementById('toggleBtn').textContent=b.style.display==='none'?'[+]':'[-]' }; document.getElementById('btnSave').onclick=()=>{ config.apiUrl=document.getElementById('cfgUrl').value.trim(); config.apiKey=document.getElementById('cfgKey').value.trim(); config.model=document.getElementById('cfgModel').value.trim(); config.autoAnswer=document.getElementById('cfgAuto').checked; config.lyckToken=document.getElementById('cfgLyckToken').value.trim(); config.autoSubmit=document.getElementById('cfgAutoSubmit').checked; config.autoNext=document.getElementById('cfgAutoNext').checked; Object.entries(config).forEach(([k,v])=>GM_setValue(k,v)); updateStatus('✅ 已保存','green'); if(config.autoAnswer) startMonitor(); }; document.getElementById('btnRun').onclick=()=>{runFlag=!0;batchAbort=!1;processAll()}; document.getElementById('btnPause').onclick=()=>{runFlag=!1;batchAbort=!0;isBatchRunning=!1;updateStatus('⏸ 暂停','red')}; document.getElementById('btnClear').onclick=()=>{ answeredCache.clear();persistCache(); updateStatus('🗑 缓存已清空','#9c27b0'); }; // === 刷课模块事件 === document.getElementById('cxvSpeed').oninput=function(){ document.getElementById('cxvSpeedLabel').textContent=this.value+'x'; config.videoSpeed=parseFloat(this.value); GM_setValue('videoSpeed',config.videoSpeed); }; document.getElementById('btnStartVideo').onclick=()=>{ if(cxv_isRunning) return addLog('⚠️ 刷课已在运行中','orange'); cxv_startCourseAuto(); }; document.getElementById('btnStopVideo').onclick=()=>{ cxv_isRunning=false; if(cxv_reportInterval){clearInterval(cxv_reportInterval);cxv_reportInterval=null;} document.getElementById('cxvStatus').textContent='刷课: 已停止'; addLog('⏹ 已停止刷课','red'); }; document.getElementById('btnViewRecords').onclick=showAnswerRecords; } function updateStatus(t, c='#666') { const e=document.getElementById('aiStatus'); if(e){e.textContent=`状态:${t}`;e.style.color=c} addLog(t,c); } function addLog(t,c='#333') { const e=document.getElementById('aiLogs'); if(e){const d=document.createElement('div');d.className='ai-log-item';d.style.color=c;d.textContent=`[${new Date().toLocaleTimeString('it-IT')}] ${t}`;e.appendChild(d);e.scrollTop=e.scrollHeight} console.log(`[AI] ${t}`); } // ======================== ====== 答案记录弹窗 ============================ function showAnswerRecords() { const records = loadAnswerRecords(); if (!records.length) { addLog('📝 暂无答案记录','orange'); return; } // 移除旧弹窗 const old = document.getElementById('ai-records-overlay'); if (old) old.remove(); const overlay = document.createElement('div'); overlay.id = 'ai-records-overlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999999;display:flex;align-items:center;justify-content:center'; let html = '
'; html += '
📝 答案记录(近200条)×
'; records.forEach((r, i) => { html += `
`; html += `
${r.time} | 来源: ${r.source}
`; html += `
题目: ${r.question}
`; html += `
答案: ${r.answer}
`; }); html += '
'; overlay.innerHTML = html; document.body.appendChild(overlay); document.getElementById('ai-records-close').onclick = () => overlay.remove(); overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; } // ======================== ====== 页面路由 ============================ function detectPageContext() { const url = location.href; const path = location.pathname; if (path.includes('knowledge/cards') && !document.querySelector('.mark_answer,.mark_score,.resultNum,.score')) return 'COURSE_PAGE'; if (path.includes('work/phone/doHomeWork')) return 'WORK_PAGE'; if (path.includes('mycourse/studentstudy')) return 'STUDY_PAGE'; if (path.includes('work/phone/selectWorkQuestionYiPiYue')) return 'REVIEW_PAGE'; if (document.querySelector('.questionLi,.TiMu,.topic_item')) return 'ANSWER_PAGE'; return 'UNKNOWN'; } // ======================== ====== 背景保活 ============================ let cxv_keepAliveCtx = null; function startKeepAlive() { try { if (!cxv_keepAliveCtx) { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return; cxv_keepAliveCtx = new AC(); const osc = cxv_keepAliveCtx.createOscillator(); const gain = cxv_keepAliveCtx.createGain(); gain.gain.value = 0.01; osc.connect(gain); gain.connect(cxv_keepAliveCtx.destination); osc.start(); } if (cxv_keepAliveCtx.state === 'suspended') cxv_keepAliveCtx.resume(); } catch(e) {} } // ======================== ====== 字体解密(font-cxsecret) ============================ // 超星使用自定义字体加密文字,需解密后才能正确匹配答案 function cxDecryptFont(iframeDocument) { try { const doc = iframeDocument || document; const styleEl = Array.from(doc.querySelectorAll('style')).find(s => s.textContent && s.textContent.includes('font-cxsecret')); if (!styleEl) return; const fontMatch = styleEl.textContent.match(/base64,([\w\W]+?)'/); if (!fontMatch) return; const fontData = fontMatch[1]; const fontArr = base64ToUint8Array(fontData); const font = parseTyprFont(fontArr); if (!font) return; const table = JSON.parse(GM_getResourceText('ttf')); if (!table) return; const match = {}; for (let i = 19968; i < 40870; i++) { const gid = TyprU.codeToGlyph(font, i); if (!gid) continue; const path = TyprU.glyphToPath(font, gid); if (!path) continue; const hash = md5(JSON.stringify(path)).slice(24); if (table[hash]) match[i] = table[hash]; } doc.querySelectorAll('.font-cxsecret').forEach(el => { let html = el.innerHTML; for (const [key, val] of Object.entries(match)) { html = html.replace(new RegExp(String.fromCharCode(Number(key)), 'g'), String.fromCharCode(val)); } el.innerHTML = html; el.classList.remove('font-cxsecret'); }); addLog('🔤 字体解密完成','#4caf50'); } catch(e) { console.log('字体解密失败:', e); } } function base64ToUint8Array(base64) { const data = atob(base64); const buf = new Uint8Array(data.length); for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i); return buf; } // 精简版 Typr 字体解析器(仅用于 font-cxsecret 解密) var TyprU = {}; TyprU.codeToGlyph = function(font, code) { const cmap = font.cmap; if (!cmap) return 0; const tabs = ['p3e10','p0e4','p3e1','p1e0']; for (const key of tabs) { const idx = cmap[key]; if (idx === undefined) continue; const tab = cmap.tables[idx]; if (!tab) continue; if (tab.format === 4) { for (let i = 0; i < tab.endCount.length; i++) { if (code <= tab.endCount[i]) { if (code < tab.startCount[i]) break; if (tab.idRangeOffset[i] !== 0) { const offset = tab.idRangeOffset[i] >> 1; const base = i - tab.idRangeOffset.length + offset; if (base + code - tab.startCount[i] < tab.glyphIdArray.length) return tab.glyphIdArray[code - tab.startCount[i] + base]; } else { return (code + tab.idDelta[i]) & 0xFFFF; } } } } else if (tab.format === 12 && tab.groups) { for (const g of tab.groups) { if (g[0] <= code && code <= g[1]) return g[2] + (code - g[0]); } } } return 0; }; var TyprP = { moveTo: function(p,x,y){ p.cmds.push('M'); p.crds.push(x,y); }, lineTo: function(p,x,y){ p.cmds.push('L'); p.crds.push(x,y); }, curveTo: function(p,a,b,c,d,e,f){ p.cmds.push('C'); p.crds.push(a,b,c,d,e,f); }, qcurveTo: function(p,a,b,c,d){ p.cmds.push('Q'); p.crds.push(a,b,c,d); }, closePath: function(p){ p.cmds.push('Z'); } }; TyprU.glyphToPath = function(font, gid) { const path = { cmds: [], crds: [] }; if (!font.glyf) return path; const loca = font.loca, data = font._data, glyfOff = font._glyfOff; if (!loca || !data) return path; const off = glyfOff + loca[gid]; if (loca[gid] === loca[gid+1]) return path; try { const bin = TyprB; const noc = bin.readShort(data, off); if (noc > 0) { let o = off + 10; const endPts = bin.readUshorts(data, o, noc); o += noc * 2; const insLen = bin.readUshort(data, o); o += 2 + insLen; const ptCount = endPts[noc - 1] + 1; const flags = []; for (let i = 0; i < ptCount; i++) { const f = data[o++]; flags.push(f); if (f & 8) { const r = data[o++]; for (let j = 0; j < r; j++) { flags.push(f); i++; } } } const xs = [], ys = []; let x = 0, y = 0; for (let i = 0; i < ptCount; i++) { const f = flags[i], i8 = !!(f & 2), same = !!(f & 16); if (i8) { x += same ? data[o] : -data[o]; o++; } else { x += same ? 0 : bin.readShort(data, o); o += same ? 0 : 2; } xs.push(x); } for (let i = 0; i < ptCount; i++) { const f = flags[i], i8 = !!(f & 4), same = !!(f & 32); if (i8) { y += same ? data[o] : -data[o]; o++; } else { y += same ? 0 : bin.readShort(data, o); o += same ? 0 : 2; } ys.push(y); } for (let c = 0; c < noc; c++) { const i0 = c === 0 ? 0 : endPts[c-1] + 1, il = endPts[c]; for (let i = i0; i <= il; i++) { const pr = i === i0 ? il : i - 1, nx = i === il ? i0 : i + 1; const on = !!(flags[i] & 1), prOn = !!(flags[pr] & 1), nxOn = !!(flags[nx] & 1); if (i === i0) { if (on) { if (prOn) TyprP.moveTo(path, xs[pr], ys[pr]); else TyprP.moveTo(path, xs[i], ys[i]); } else TyprP.moveTo(path, prOn ? xs[pr] : (xs[pr]+xs[i])/2, prOn ? ys[pr] : (ys[pr]+ys[i])/2); } if (on) { if (prOn) TyprP.lineTo(path, xs[i], ys[i]); } else { if (nxOn) TyprP.qcurveTo(path, xs[i], ys[i], xs[nx], ys[nx]); else TyprP.qcurveTo(path, xs[i], ys[i], (xs[i]+xs[nx])/2, (ys[i]+ys[nx])/2); } } TyprP.closePath(path); } } } catch(e) { /* ignore parse errors */ } return path; }; var TyprB = { readShort: function(d,o) { const v = new DataView(d.buffer, d.byteOffset, d.byteLength); return v.getInt16(o); }, readUshort: function(d,o) { const v = new DataView(d.buffer, d.byteOffset, d.byteLength); return v.getUint16(o); }, readUshorts: function(d,o,l) { const r = []; for (let i = 0; i < l; i++) r.push(TyprB.readUshort(d, o + i*2)); return r; }, readUint: function(d,o) { const v = new DataView(d.buffer, d.byteOffset, d.byteLength); return v.getUint32(o); }, readASCII: function(d,o,l) { let s = ''; for (let i = 0; i < l; i++) s += String.fromCharCode(d[o+i]); return s; }, readFixed: function(d,o) { return (d[o]<<8|d[o+1]) + (d[o+2]<<8|d[o+3]) / (256*256+4); } }; function parseTyprFont(data) { try { const bin = TyprB; const tag = bin.readASCII(data, 0, 4); if (tag !== 'OTTO' && tag !== 'true' && tag !== '\x00\x01\x00\x00' && tag !== 'ttcf') { // 尝试解析 } const numTables = bin.readUshort(data, 4); const font = { _data: data, _offset: 0 }; const tabs = {}; let off = 12; for (let i = 0; i < numTables; i++) { const tag = bin.readASCII(data, off, 4); off += 4; bin.readUint(data, off); off += 4; const toff = bin.readUint(data, off); off += 4; const len = bin.readUint(data, off); off += 4; tabs[tag] = { offset: toff, length: len }; } if (tabs.cmap) font.cmap = parseCmap(data, tabs.cmap.offset, tabs.cmap.length); if (tabs.loca) font.loca = parseLoca(data, tabs.loca.offset, tabs.loca.length, font); if (tabs.glyf) { font.glyf = []; font._glyfOff = tabs.glyf.offset; } if (tabs.head) font.head = { indexToLocFormat: bin.readShort(data, tabs.head.offset + 50) }; if (tabs.maxp) font.maxp = { numGlyphs: bin.readUshort(data, tabs.maxp.offset + 4) }; if (tabs.hmtx) font.hmtx = { aWidth: [] }; return font; } catch(e) { return null; } } function parseCmap(data, offset) { const bin = TyprB; const numTables = bin.readUshort(data, offset + 2); const cmap = { tables: [] }; let off = offset + 4; const map = {}; for (let i = 0; i < numTables; i++) { const pid = bin.readUshort(data, off); off += 2; const eid = bin.readUshort(data, off); off += 2; const subOff = bin.readUint(data, off); off += 4; const key = 'p' + pid + 'e' + eid; const idx = cmap.tables.length; const fmt = bin.readUshort(data, offset + subOff); let subt; if (fmt === 4) subt = parseCmap4(data, offset + subOff); else if (fmt === 12) subt = parseCmap12(data, offset + subOff); else continue; cmap.tables.push(subt); map[key] = idx; } Object.assign(cmap, map); return cmap; } function parseCmap4(data, offset) { const bin = TyprB; const segCount = bin.readUshort(data, offset + 6) >> 1; let o = offset + 14; const endCount = bin.readUshorts(data, o, segCount); o += segCount * 2 + 2; const startCount = bin.readUshorts(data, o, segCount); o += segCount * 2; const idDelta = []; for (let i = 0; i < segCount; i++) { idDelta.push(bin.readShort(data, o)); o += 2; } const idRangeOffset = bin.readUshorts(data, o, segCount); o += segCount * 2; const glyphIdArray = []; while (o < offset + 1024) { try { glyphIdArray.push(bin.readUshort(data, o)); o += 2; } catch(e) { break; } } return { format: 4, endCount, startCount, idDelta, idRangeOffset, glyphIdArray }; } function parseCmap12(data, offset) { const bin = TyprB; const nGroups = bin.readUint(data, offset + 12); const groups = []; let o = offset + 16; for (let i = 0; i < nGroups; i++) { groups.push([bin.readUint(data, o), bin.readUint(data, o + 4), bin.readUint(data, o + 8)]); o += 12; } return { format: 12, groups }; } function parseLoca(data, offset, length, font) { const headOff = font._offset + (font._headOff || 0); const ver = font.head ? font.head.indexToLocFormat : 0; const num = font.maxp ? font.maxp.numGlyphs : 0; const loca = []; if (ver === 0) for (let i = 0; i <= num && i * 2 + offset < data.length; i++) loca.push(TyprB.readUshort(data, offset + i * 2) << 1); else for (let i = 0; i <= num && i * 4 + offset < data.length; i++) loca.push(TyprB.readUint(data, offset + i * 4)); return loca; } // ======================== ====== 人脸识别检测 ============================ let cx_faceCheckTimer = null; function startFaceRecognitionCheck() { if (cx_faceCheckTimer) return; cx_faceCheckTimer = setInterval(() => { try { const docs = [document]; document.querySelectorAll('iframe').forEach(f => { try { if (f.contentDocument) docs.push(f.contentDocument); } catch(e) {} }); for (const doc of docs) { // 检测二维码弹窗 if (doc.querySelector('#fcqrimg[src]') || doc.querySelector('.chapterVideoFaceMaskDiv')) { addLog('⚠️ 检测到人脸识别,请扫码完成验证后继续...', '#ff9800'); return; } // 检测文字提示 const bodyText = (doc.body ? doc.body.innerText : '') || ''; if (bodyText.includes('人脸信息采集') || bodyText.includes('采集人脸信息')) { addLog('⚠️ 检测到人脸识别,请完成验证', '#ff9800'); return; } } } catch(e) {} }, 5000); } // ======================== ====== 答案记录系统 ============================ const ANSWER_RECORDS_KEY = 'ai_answer_records'; function loadAnswerRecords() { try { return JSON.parse(GM_getValue(ANSWER_RECORDS_KEY, '[]') || '[]'); } catch(e) { return []; } } function saveAnswerRecord(question, answer, source) { const records = loadAnswerRecords(); records.unshift({ question: (question||'').slice(0,100), answer: (answer||'').slice(0,200), source: source||'unknown', time: new Date().toLocaleString('zh-CN') }); if (records.length > 200) records.length = 200; GM_setValue(ANSWER_RECORDS_KEY, JSON.stringify(records)); } // ======================== ====== API ============================ function askAI(prompt) { return new Promise((resolve,reject)=>{ let u=config.apiUrl.trim().replace(/\/+$/,''); if(!u.endsWith('/chat/completions')) u+='/chat/completions'; GM_xmlhttpRequest({ method:'POST',url:u, headers:{'Content-Type':'application/json','Authorization':`Bearer ${config.apiKey}`}, data:JSON.stringify({ model:config.model, messages:[{role:'system',content:'思政答题。单选输出A/B/C/D;多选连续如ABC;判断A=对B=错;填空|分隔;简答核心文字。仅答案无多余字'},{role:'user',content:prompt}], temperature:0.1,max_tokens:512 }), onload(r){ if(r.status!==200) return reject(`HTTP${r.status}`); try{ const raw=r.responseText.trim(); let j=JSON.parse(raw); if(j.choices?.[0]?.message?.content) return resolve(j.choices[0].message.content.trim()); let ft='';raw.split('\n').forEach(l=>{l=l.trim();if(!l.startsWith('data:')||l==='data: [DONE]')return;const c=JSON.parse(l.slice(5).trim());if(c.choices?.[0]?.delta?.content) ft+=c.choices[0].delta.content}); ft?resolve(ft.trim()):reject('AI无答案'); }catch(e){reject(`解析失败:${e.message}`)} }, onerror:()=>reject('网络异常') }); }); } // ======================== ====== 免费题库查询(先题库,后AI) ============================ function qbApibyteCount() { return parseInt(GM_getValue('qb_apibyte_count', 0)) || 0; } function qbApibyteQuotaOk() { const today = new Date().toISOString().slice(0, 10); const d = GM_getValue('qb_apibyte_date', ''); if (d !== today) { GM_setValue('qb_apibyte_date', today); GM_setValue('qb_apibyte_count', 0); return true; } return qbApibyteCount() < APIBYTE_DAILY_LIMIT; } function qbApibyteIncr() { GM_setValue('qb_apibyte_count', qbApibyteCount() + 1); } function searchQuestionBank(title) { return new Promise((resolve) => { let resolved = false; // 6秒总超时:三个接口宕机时快速降级到AI const totalTimer = setTimeout(() => { if (!resolved) { resolved = true; resolve(null); } }, 6000); const apis = [ { name: 'Muketool', url: `https://api.muketool.com?question=${encodeURIComponent(title)}` }, { name: 'lyck6', url: `http://cx.lyck6.cn/api/api.php?question=${encodeURIComponent(title)}` }, ]; // apibyte 每日额度用完后自动跳过 if (qbApibyteQuotaOk()) { apis.push({ name: 'apibyte', url: `https://apione.apibyte.cn/edusearch?question=${encodeURIComponent(title)}&platform=chaoxing` }); } else { addLog(`📚 题库[apibyte]今日额度已用完(${APIBYTE_DAILY_LIMIT}次),跳过`, 'orange'); } let idx = 0; function tryNext() { if (resolved || idx >= apis.length) { clearTimeout(totalTimer); return resolve(null); } const api = apis[idx++]; addLog(`📚 查询题库[${api.name}]...`, '#9c27b0'); GM_xmlhttpRequest({ method: 'GET', url: api.url, timeout: 4000, onload: (r) => { if (api.name === 'apibyte') qbApibyteIncr(); const ans = parseQbResponse(r.responseText); if (ans) { addLog(`📚 题库[${api.name}]命中: ${ans}`, '#4caf50'); resolved = true; clearTimeout(totalTimer); resolve(ans); } else { addLog(`📚 题库[${api.name}]未命中,尝试下一个`, 'orange'); tryNext(); } }, onerror: () => { if (api.name === 'apibyte') qbApibyteIncr(); addLog(`⚠️ 题库[${api.name}]网络异常`, 'orange'); tryNext(); }, ontimeout: () => { if (api.name === 'apibyte') qbApibyteIncr(); addLog(`⚠️ 题库[${api.name}]超时`, 'orange'); tryNext(); } }); } tryNext(); }); } function parseQbResponse(text) { if (!text) return null; const t = text.trim(); if (!t || t === 'null' || t === 'undefined') return null; try { const j = JSON.parse(t); const candidates = ['answer','data','result','msg','answers','option','content','answer_text','Answer','answerStr','answer_str','ans','correct','correctAnswer']; for (const key of candidates) { const v = j[key]; if (v !== undefined && v !== null) { const s = typeof v === 'string' ? v : typeof v === 'number' ? String(v) : null; if (s && s.length > 0 && s.length < 200) return s; } } // data 或 result 为数组时提取 const arrFields = ['data', 'result', 'answers', 'list']; for (const f of arrFields) { const arr = j[f]; if (Array.isArray(arr) && arr.length) { const items = arr.map(i => typeof i === 'string' ? i : i.answer || i.option || i.content || i.name || i.value || i.correct || '').filter(Boolean); if (items.length) return items.join('|'); } } // 递归扫描深层对象 function deepScan(obj, depth = 0) { if (depth > 3 || !obj || typeof obj !== 'object') return null; for (const val of Object.values(obj)) { if (typeof val === 'string' && val.length > 0 && val.length < 200) return val; const r = deepScan(val, depth + 1); if (r) return r; } return null; } const deep = deepScan(j); if (deep) return deep; } catch(e) {} // 纯文本短答案直接返回 if (t.length > 0 && t.length < 100 && /^[A-Da-d\|\d\.一-龥]+$/.test(t)) return t.toUpperCase(); return null; } // ======================== ====== 题目检测 ============================ function findQuestions() { const sels=['.questionLi','.TiMu','.topic_item','.question_item','[class*="question"]','[class*="TiMu"]','.Zy_ListTi','.shiti','.mark_li','.exam-topic-item','.test-item']; for(const s of sels){ const n=document.querySelectorAll(s),v=Array.from(n).filter(x=>x.innerText.trim().length>8); if(v.length) return v; } // 深度扫描 const set=new Set(); document.querySelectorAll('div,li').forEach(el=>{ if(el.querySelectorAll('input[type=radio],input[type=checkbox],textarea').length&&el.innerText.trim().length>15) set.add(el); }); return [...set]; } function qId(q){ const d=q.getAttribute('data')||q.getAttribute('data-id')||q.getAttribute('id')||''; if(d) return d; return extractType(q)+'|'+extractTitle(q).substring(0,45); } function extractTitle(q){ const s=['.mark_name','.Zy_TItle .clearfix','.Zy_TItle','.topic_name','.question_name','.title','.TiMuTitle','.mark_tit','h3','h4','.qt-content','[class*=title]']; for(const sel of s){const d=q.querySelector(sel);if(d&&d.innerText.trim().length>3) return d.innerText.replace(/\s+/g,' ').trim()} return q.innerText.trim().split('\n')[0].trim(); } function extractType(q){ let t=q.getAttribute('typeName')||''; if(!t){const s=['.colorShallow','.Zy_TItle i','.mark_type','.topic-type'];for(const sel of s){const d=q.querySelector(sel);if(d&&d.innerText.trim()){t=d.innerText.trim();break}}} if(!t){const m=extractTitle(q).match(/[【\[]\s*(单选|多选|判断|填空|简答|论述)\s*[】\]]/);if(m) t=m[1]} if(!t){const r=q.querySelectorAll('input[type=radio]').length,c=q.querySelectorAll('input[type=checkbox]').length,t2=q.querySelectorAll('textarea,input[type=text]').length;if(r>=2) t='单选题';else if(c>0) t='多选题';else if(t2>0) t='填空题'} return t||'未知'; } function extractOptions(q){ const s=['ul.Zy_ulTop li','.answerBg','.option-item','li[class*=option]','div[style*=margin] label','div[class*=opt]']; for(const sel of s){const items=q.querySelectorAll(sel);if(items.length>=2){let txt='';items.forEach(i=>{const t=i.innerText.trim();if(t) txt+=t+'\n'});if(txt.trim()) return txt.trim()}} return ''; } // ======================== ====== 【核心修复】多防线已答检测 ============================ function isAnswered(q) { const type = extractType(q); if (/判断|单选/.test(type)) { if (q.querySelectorAll('input[type="radio"]:checked').length > 0) return true; if (q.querySelectorAll('input[type="radio"][checked]').length > 0) return true; if (q.querySelectorAll('.selected, .active, .on, .cur, .check_answer_bg, .check_answer, .has-answer, .answered').length > 0) return true; if (q.querySelectorAll('li.selected, li.active, li.on, li.cur').length > 0) return true; } if (/多选/.test(type)) { if (q.querySelectorAll('input[type="checkbox"]:checked').length > 0) return true; if (q.querySelectorAll('input[type="checkbox"][checked]').length > 0) return true; if (q.querySelectorAll('.check_answer_bg, .check_answer, .has-answer, .answered, .selected, .active').length > 0) return true; const lis = q.querySelectorAll('li'); for (const li of lis) { const inp = li.querySelector('input[type="checkbox"]'); if (inp && (inp.checked || inp.hasAttribute('checked'))) return true; } } if (/填空/.test(type)) { const inputs = q.querySelectorAll('input[type="text"], textarea'); for (const inp of inputs) { if (inp.value && inp.value.trim().length > 0) return true; } const eds = q.querySelectorAll('[contenteditable="true"]'); for (const ed of eds) { if (ed.textContent && ed.textContent.trim().length > 0) return true; } } if (/简答|论述|案例分析/.test(type)) { const ta = q.querySelector('textarea'); if (ta && ta.value && ta.value.trim().length > 0) return true; try { const ifr = q.querySelector('iframe'); if (ifr && ifr.contentDocument && ifr.contentDocument.body && ifr.contentDocument.body.innerText.trim().length > 0) return true; } catch(e) {} } if (q.querySelectorAll('input:checked').length > 0) return true; if (q.querySelectorAll('input[checked]').length > 0) return true; return false; } // ======================== ====== 【核心修复】预扫描 ============================ function preScanAnswered() { const qs = findQuestions(); let newlyCached = 0; qs.forEach(q => { const id = qId(q); if (!answeredCache.has(id) && isAnswered(q)) { answeredCache.add(id); newlyCached++; } }); if (newlyCached > 0) { persistCache(); addLog(`📌 预扫描:缓存 ${newlyCached} 道已有答案的题目`, '#4caf50'); } return qs.length; } // ======================== ====== 【核心修复】fillAnswer 带保护机制 ============================ function fillAnswer(qNode, qType, aiAns) { if (!aiAns) return false; if (isAnswered(qNode)) { addLog('🛡️ 保护触发:即将填答案时发现题目已有答案,跳过', '#ff9800'); return false; } if (/单选|多选|判断/.test(qType)) { const targetChars = aiAns.toUpperCase().replace(/[^A-Z]/g, '').split(''); if (!targetChars.length) return false; let snapshotBefore = []; if (/多选/.test(qType)) { qNode.querySelectorAll('input[type="checkbox"]').forEach(inp => { if (inp.checked) snapshotBefore.push(inp); }); } const opts = qNode.querySelectorAll('ul li, label, .option-item, .answerBg, div[style*="margin"]'); let clickedAny = false; opts.forEach(optDom => { const input = optDom.querySelector('input[type="radio"],input[type="checkbox"]'); if (input && input.checked) return; let letter = ''; const ld = optDom.querySelector('i.fl, b, span, .letter, .mark_letter'); if (ld) letter = ld.innerText.trim().replace(/[^A-Z]/g, ''); if (!letter) { const m = optDom.innerText.trim().match(/^([A-D])[.、\s]/); if (m) letter = m[1]; } if (!letter && ['A','B','C','D'].includes(optDom.innerText.trim())) letter = optDom.innerText.trim(); if (letter && targetChars.includes(letter)) { if (input && !input.checked) { if (isAnswered(qNode)) { addLog('🛡️ 二次保护:点击前发现已答,停止填充', '#ff9800'); return; } input.click(); ['input','change','click'].forEach(ev => input.dispatchEvent(new Event(ev, {bubbles:true}))); clickedAny = true; } else if (!input) { optDom.click(); clickedAny = true; } } }); if (/多选/.test(qType) && snapshotBefore.length > 0) { setTimeout(() => { snapshotBefore.forEach(inp => { if (inp && !inp.checked) { inp.checked = true; ['change','input'].forEach(ev => inp.dispatchEvent(new Event(ev, {bubbles:true}))); addLog('🛡️ 恢复:保护多选已选项未被清空', '#4caf50'); } }); }, 200); } return clickedAny; } else if (/填空/.test(qType)) { const parts = aiAns.split('|').map(s=>s.trim()).filter(Boolean); if (!parts.length) return false; const inputs = qNode.querySelectorAll('input[type=text], textarea'); inputs.forEach((inp,i)=>{ if (inp.value && inp.value.trim()) return; if (parts[i]) { inp.value = parts[i]; ['input','change','keyup','blur'].forEach(ev => inp.dispatchEvent(new Event(ev, {bubbles:true}))); } }); return true; } else { const ta = qNode.querySelector('textarea'); if (ta && ta.value && ta.value.trim()) return false; if (ta) { ta.value = aiAns; ['input','change','keyup','blur'].forEach(ev => ta.dispatchEvent(new Event(ev, {bubbles:true}))); return true; } return false; } } // ======================== ====== 单题处理 ============================ async function processOne(qNode, idx) { if (!runFlag || batchAbort) return false; const id = qId(qNode); if (answeredCache.has(id)) { addLog(`⏭ ${idx}:缓存已有,跳过`, '#888'); return false; } if (isAnswered(qNode)) { answeredCache.add(id); persistCache(); addLog(`⏭ ${idx}:页面已有答案,跳过并缓存`, '#888'); return false; } if (answeringSet.has(id)) return false; answeringSet.add(id); const title = extractTitle(qNode); const typeText = extractType(qNode); const options = extractOptions(qNode); addLog(`📝 ${idx} [${typeText}] ${title.substring(0,60)}`, '#2196f3'); qNode.classList.add('ai-highlight-answering'); let rule = ''; if (/单选/.test(typeText)) rule = '仅输出单个大写字母A/B/C/D'; else if (/多选/.test(typeText)) rule = '全部正确字母连续输出无分隔'; else if (/判断/.test(typeText)) rule = '正确A错误B,仅一个字母'; else if (/填空/.test(typeText)) rule = '多空用|分隔'; else rule = '简短核心文字'; const prompt = `【题型】${typeText}\n【题干】${title}\n${options?'【选项】\n'+options+'\n':''}要求:${rule}`; try { // === 先查免费题库,命中则跳过AI === let ans = await searchQuestionBank(title); let qbHit = true; if (!ans) { addLog(`🤖 题库均未命中,调用AI...`, '#ff9800'); ans = await askAI(prompt); qbHit = false; } addLog(`✅ ${idx} [${qbHit ? '题库' : 'AI'}] ${ans}`, 'green'); if (isAnswered(qNode)) { addLog(`🛡️ ${idx} 填入前发现已被其他操作回答,跳过`, '#ff9800'); answeredCache.add(id); persistCache(); return false; } fillAnswer(qNode, typeText, ans); answeredCache.add(id); persistCache(); // 记录答案 saveAnswerRecord(title, ans, qbHit ? '题库' : 'AI'); qNode.classList.remove('ai-highlight-answering'); qNode.classList.add('ai-highlight-done'); await delay(800 + Math.random() * 1200); return true; } catch (err) { addLog(`❌ ${idx} 失败: ${err}`, 'red'); qNode.classList.remove('ai-highlight-answering'); await delay(800); return false; } finally { answeringSet.delete(id); } } // ======================== ====== 主流程 ============================ async function processAll() { if (!config.apiKey) return updateStatus('❌ 请先在面板配置 API Key','red'); if (isBatchRunning) return addLog('已有任务在运行','orange'); if (!runFlag) runFlag = true; isBatchRunning = true; batchAbort = false; try { preScanAnswered(); const qs = findQuestions(); if (!qs.length) return updateStatus('未找到题目','red'); updateStatus(`📊 共 ${qs.length} 题,缓存 ${answeredCache.size} 题,开始...`,'blue'); for (let i = 0; i < qs.length; i++) { if (!runFlag || batchAbort) { updateStatus('⏸ 已暂停','red'); break; } await processOne(qs[i], i + 1); } if (!batchAbort) { markNumbers(qs); updateStatus(`🎉 完成!缓存 ${answeredCache.size} 题`,'green'); const left = qs.filter(q => !isAnswered(q) && !answeredCache.has(qId(q))); if (left.length) addLog(`⚠️ 仍有 ${left.length} 题未完成,可能需手动处理`,'orange'); } } catch(e) { updateStatus(`❌ ${e.message}`,'red'); } finally { isBatchRunning = false; } } function markNumbers(qs) { const targetNums = new Set(); qs.forEach((q,i)=>{ if (isAnswered(q)) targetNums.add(i + 1); }); const boxes = document.querySelectorAll('div[style*="width:40px"],div[style*="width:36px"],span[class*="num"],div[class*="index"]'); boxes.forEach(box => { const txt = box.innerText.trim(); if (targetNums.has(Number(txt))) { box.click(); } }); addLog(`🔢 标记 ${targetNums.size} 题完成`,'#228B22'); } function delay(ms) { return new Promise(r => setTimeout(r, ms)); } // ======================== ====== 实时监听 ============================ let observer = null; function startMonitor() { if (!config.autoAnswer) { updateStatus('自动答题未开启','orange'); return; } if (observer) observer.disconnect(); addLog('📡 实时监听启动','#4caf50'); updateStatus(`自动模式,缓存${answeredCache.size}题`,'#4caf50'); let timer = null; observer = new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => { if (!config.autoAnswer || !runFlag || isBatchRunning) return; const qs = findQuestions(); const un = qs.filter(q => { const id = qId(q); return !answeredCache.has(id) && !answeringSet.has(id) && !isAnswered(q); }); if (un.length > 0) processAll(); }, 1000); }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { if (config.autoAnswer && runFlag) processAll(); }, 2000); setInterval(() => { if (!config.autoAnswer || !runFlag || isBatchRunning) return; const qs = findQuestions(); const un = qs.filter(q => { const id = qId(q); return !answeredCache.has(id) && !answeringSet.has(id) && !isAnswered(q); }); if (un.length > 0) processAll(); }, 6000); } // ======================== ====== 刷课模块-主入口 ============================ // 在 knowledge/cards 页面自动处理视频+章节测验+导航 let cxv_logTimer = null; function cxv_startCourseAuto() { if (detectPageContext() !== 'COURSE_PAGE') { // 不在课程页面,尝试找题目回答 addLog('📋 非课程页面,切换到答题模式','orange'); processAll(); return; } cxv_isRunning = true; document.getElementById('cxvStatus').textContent='刷课: 运行中...'; addLog('▶ 开始刷课...','#4caf50'); cxv_missionList = []; cxv_currentIdx = 0; // 扫描页面上的任务卡片 cxv_scanTasks(); if (cxv_missionList.length === 0) { addLog('⚠️ 未找到视频或测验任务','orange'); cxv_isRunning = false; document.getElementById('cxvStatus').textContent='刷课: 无任务'; return; } addLog(`📊 发现 ${cxv_missionList.length} 个任务:${cxv_missionList.filter(i=>i.type==='video').length}个视频, ${cxv_missionList.filter(i=>i.type==='work').length}个测验`,'#2196f3'); cxv_processNext(); } function cxv_scanTasks() { cxv_missionList = []; // 1. 扫描视频/文档卡片 const cards = document.querySelectorAll('.clearfix .posCatalog_select, .tabtags span[style*="cursor:pointer"]'); // 对于新UI,尝试从附件列表中检测 const attachments = document.querySelectorAll('[class*="attachment"], [class*="job"], .jobItem, .point-text'); attachments.forEach(el => { const text = el.textContent.trim(); const parent = el.closest('.clearfix, .catalog_points, .chapterItem, li, div'); if (!parent) return; // 跳过已完成的任务 if (parent.querySelector('.icon_Completed, .finish, .right-icon') && config.autoNext) return; const jobid = el.getAttribute('data-id') || el.getAttribute('id') || ''; const name = el.getAttribute('title') || text; if (text.includes('视频') || text.includes('音')) { cxv_missionList.push({ type:'video', el, name, jobid, done:false }); } else if (/章节|测验|测试|作业|考试/.test(text)) { cxv_missionList.push({ type:'work', el, name, jobid, done:false }); } else if (/文档|PPT|课件|阅读|教材|图书|讲义|word|pdf/.test(text)) { cxv_missionList.push({ type:'document', el, name, jobid, done:false }); } }); // 备用:直接找所有 .point-text 元素 if (cxv_missionList.length === 0) { document.querySelectorAll('.point-text, .point_name').forEach(el => { const text = el.textContent.trim(); const card = el.closest('.catalog_points, .chapterItem, .clearfix'); if (!card || card.querySelector('.icon_Completed')) return; if (/视频|音频|录音/.test(text)) { cxv_missionList.push({ type:'video', el, name:text, jobid:'', done:false }); } else if (/作业|测验|测试|考试|章节/.test(text)) { cxv_missionList.push({ type:'work', el, name:text, jobid:'', done:false }); } else if (/文档|PPT|课件|阅读|教材|图书|word|pdf/.test(text)) { cxv_missionList.push({ type:'document', el, name:text, jobid:'', done:false }); } }); } } async function cxv_processNext() { while (cxv_currentIdx < cxv_missionList.length && cxv_isRunning) { const task = cxv_missionList[cxv_currentIdx]; if (task.done) { cxv_currentIdx++; continue; } addLog(`📋 [${cxv_currentIdx+1}/${cxv_missionList.length}] 处理: ${task.name}`,`#2196f3`); document.getElementById('cxvStatus').textContent=`刷课: ${task.name}`; try { if (task.type === 'video') { await cxv_doVideo(task); } else if (task.type === 'work') { await cxw_doWork(task); } else if (task.type === 'document') { await cxv_doDocument(task); } task.done = true; } catch(e) { addLog(`❌ ${task.name} 失败: ${e.message}`,'red'); // 失败也标记已处理,避免卡住 task.done = true; } cxv_currentIdx++; // 任务间延时 await delay(1500); } // 全部完成 if (cxv_isRunning) { addLog('🎉 所有任务处理完毕!','green'); document.getElementById('cxvStatus').textContent='刷课: 完成'; cxv_isRunning = false; // 自动下一节 if (config.autoNext) cxn_skipNext(); } } // ======================== ====== 刷课模块-视频(增强版) ============================ async function cxv_doVideo(task) { // 尝试点击视频卡片打开 if (task.el) { task.el.click(); await delay(2000); } // 查找video元素 const video = document.querySelector('video'); if (!video) { const ifr = document.querySelector('iframe[src*="ananas"], iframe[src*="video"]'); if (ifr) { addLog('🎬 视频在iframe中','orange'); // 启动iframe内视频弹题自动跳过 const quizTimer = setInterval(() => { try { const idoc = ifr.contentDocument; if (idoc) { const quiz = idoc.querySelector('.ans-timelineobjects, #videoquiz-submit'); if (quiz && quiz.offsetParent !== null) { quiz.style.display = 'none'; const v = idoc.querySelector('video'); if (v && v.paused) v.play(); } } } catch(e) {} }, 2000); try { const vw = ifr.contentWindow; const vd = vw.document.querySelector('video'); if (vd) { vd.playbackRate = config.videoSpeed; vd.muted = true; vd.play(); } } catch(e) {} // 使用MutationObserver检测任务完成 const iframeParent = ifr.parentElement; if (iframeParent) { try { await Promise.race([ new Promise(r => { const obs = new MutationObserver(() => { if (iframeParent.classList.contains('ans-job-finished')) { obs.disconnect(); clearInterval(quizTimer); r(); } }); obs.observe(iframeParent, { attributes: true, attributeFilter: ['class'], subtree: true }); }), delay((video ? (video.duration || 600) : 600) * 1000 / config.videoSpeed + 30000) ]); } catch(e) {} } else { await delay(60 * 1000); } return; } addLog('⚠️ 找不到视频元素','orange'); return; } video.muted = true; video.playbackRate = config.videoSpeed; addLog(`🎬 视频已设置 ${config.videoSpeed}x 倍速播放`,'blue'); try { await video.play(); } catch(e) {} // 定期检查播放状态 + 跳过视频弹题 const checkInterval = setInterval(() => { if (!cxv_isRunning) { clearInterval(checkInterval); return; } try { video.playbackRate = config.videoSpeed; if (video.paused && !video.ended) video.play().catch(()=>{}); } catch(e) {} // 尝试关闭视频弹题 try { const quiz = document.querySelector('.ans-timelineobjects, .video-timediv, #videoquiz-submit'); if (quiz && quiz.offsetParent !== null) quiz.style.display = 'none'; } catch(e) {} }, 5000); // 等待视频结束,支持MutationObserver检测任务完成 const maxWait = Math.min(30 * 60 * 1000, (video.duration || 600) * 1000 / config.videoSpeed + 30000); const videoParent = video.closest('.ans-job-icon') ? video.closest('.ans-job-icon').parentElement : null; try { await Promise.race([ new Promise(r => { video.onended = r; video.onerror = r; }), videoParent ? new Promise(r => { const obs = new MutationObserver(() => { if (videoParent.classList.contains('ans-job-finished')) { obs.disconnect(); r(); } }); obs.observe(videoParent, { attributes: true, attributeFilter: ['class'], subtree: true }); }) : new Promise(() => {}), delay(maxWait) ]); } catch(e) {} clearInterval(checkInterval); addLog(`✅ 视频播放完成: ${task.name}`,'green'); } // ======================== ====== 刷课模块-文档自动阅读 ============================ async function cxv_doDocument(task) { if (task.el) { task.el.click(); await delay(3000); } // 处理PPT:自动翻页 const pptContainer = document.querySelector('.ppt-preview-container, .preview-section'); if (pptContainer) { addLog('📄 发现PPT文档,自动翻页...','#9c27b0'); let current = 1, total = 999; while (current < total && cxv_isRunning) { const nextBtn = pptContainer.querySelector('.next-page-btn'); if (!nextBtn) break; const totalEl = pptContainer.querySelector('.page-total'); const currentEl = pptContainer.querySelector('.page-current'); if (totalEl) total = parseInt(totalEl.textContent) || 999; if (currentEl) current = parseInt(currentEl.textContent) || current; if (current >= total) break; nextBtn.click(); await delay(2000); } addLog('✅ PPT翻页完成','green'); return; } // 处理电子书:自动滚动到底部 const bookContainer = document.querySelector('.book-container, .book-content, [class*="book"]'); if (bookContainer) { addLog('📖 发现电子书,自动滚动...','#9c27b0'); bookContainer.scrollTo({ top: bookContainer.scrollHeight, behavior: 'smooth' }); await delay(3000); addLog('✅ 电子书阅读完成','green'); return; } // 通用等待 addLog('📄 等待文档阅读...','#2196f3'); await delay(10000); } // ======================== ====== 刷课模块-章节测验 ============================ async function cxw_doWork(task) { // 点击打开测验 if (!task.el) throw new Error('找不到测验元素'); task.el.click(); await delay(3000); // 尝试在打开的页面或iframe中答题 const iframe = document.querySelector('iframe[src*="work"], iframe[src*="doHomeWork"], iframe[src*="doWork"]'); if (iframe) { addLog('📝 检测到测验iframe','#9c27b0'); try { const idoc = iframe.contentDocument || iframe.contentWindow.document; const qs = idoc.querySelectorAll('.TiMu, .questionLi'); if (qs.length > 0) { addLog(`📝 发现 ${qs.length} 道题,开始作答...`,'#9c27b0'); for (const q of qs) { if (!cxv_isRunning) break; await cxw_fillQuestion(q, idoc); await delay(1000 + Math.random()*1000); } // 尝试提交 const submitBtn = idoc.querySelector('[onclick*="submit"], [onclick*="save"], .saveYl, input[value="保存"], input[value="交卷"]'); if (submitBtn && config.autoSubmit) { submitBtn.click(); addLog('📤 已提交测验','green'); } } } catch(e) { addLog(`⚠️ iframe跨域限制:${e.message}`,'orange'); } return; } // 可能是新窗口打开的 addLog('📝 章节测验可能需要手动切换标签页','orange'); await delay(5000); } // ======================== ====== 答案相似度匹配(增强版) ============================ function calTextSimilarity(a, b) { if (!a || !b) return 0; a = String(a).replace(/\s+/g, '').replace(/[,.,。、;;::"'“”‘’!?!?()()【】<>《》]/g, '').toLowerCase(); b = String(b).replace(/\s+/g, '').replace(/[,.,。、;;::"'“”‘’!?!?()()【】<>《》]/g, '').toLowerCase(); if (a === b) return 100; if (a.includes(b) || b.includes(a)) return 90; // Levenshtein距离 const al = a.length, bl = b.length; const matrix = []; for (let i = 0; i <= al; i++) { matrix[i] = [i]; } for (let j = 0; j <= bl; j++) { matrix[0][j] = j; } for (let i = 1; i <= al; i++) { for (let j = 1; j <= bl; j++) { const cost = a[i-1] === b[j-1] ? 0 : 1; matrix[i][j] = Math.min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost); } } return Math.round((1 - matrix[al][bl] / Math.max(al, bl)) * 100); } function findBestOption(options, answer) { let best = null, bestScore = -1; const cleanAns = String(answer).replace(/\s+/g, '').toLowerCase(); options.forEach((opt, idx) => { const txt = (opt.textContent || '').replace(/\s+/g, '').toLowerCase(); // 去除选项前缀字母 const cleanOpt = txt.replace(/^[a-d][.、.\s]/i, ''); const score = calTextSimilarity(cleanAns, cleanOpt); if (score > bestScore) { bestScore = score; best = opt; } // 直接字母匹配 const letterMatch = txt.match(/^([a-d])[.、.\s]/); if (letterMatch && cleanAns.includes(letterMatch[1])) { const input = opt.querySelector('input'); if (input) bestScore = 101; best = opt; } }); return bestScore >= 60 ? best : null; } async function cxw_fillQuestion(qNode, doc) { const titleEl = qNode.querySelector('.Zy_TItle, .mark_name, h3, .topic_name, .topic-name, .question_name'); const title = titleEl ? titleEl.textContent.trim() : qNode.textContent.trim().slice(0, 150); if (!title || title.length < 3) return; // 先解密字体 cxDecryptFont(doc || document); addLog(`📝 查询: ${title.slice(0, 50)}`,'#9c27b0'); const answer = await searchQuestionBank(title); if (!answer) { addLog('⚠️ 题库未命中,跳过','orange'); return; } addLog(`🔍 答案: ${answer}`,'green'); // 记录答案 saveAnswerRecord(title, answer, '题库'); // 填答案:优先字母匹配,其次相似度匹配 const options = qNode.querySelectorAll('li, .answerBg, label, .option-item, .node_detail'); const ansLetters = answer.toUpperCase().replace(/[^A-Za-z]/g, '').split(''); let filled = false; options.forEach(opt => { const input = opt.querySelector('input[type="radio"], input[type="checkbox"]'); if (!input || input.checked) return; const txt = opt.textContent.trim(); // 字母匹配 const letterMatch = txt.match(/^([A-Da-d])\s*[.、.\s)]/); const letter = letterMatch ? letterMatch[1].toUpperCase() : ''; if (letter && ansLetters.includes(letter)) { input.click(); filled = true; return; } }); // 如果字母匹配失败,尝试相似度匹配 if (!filled) { for (const ans of ansLetters) { const optEl = findBestOption(Array.from(options), ans); if (optEl) { const inp = optEl.querySelector('input'); if (inp && !inp.checked) { inp.click(); filled = true; } } } } if (filled) addLog('✅ 答案已填入','green'); } // ======================== ====== 刷课模块-导航 ============================ function cxn_skipNext() { addLog('🔜 尝试自动下一节...','#4caf50'); try { // 新UI const nextBtns = document.querySelectorAll('.next, .nextChapter, .orientationright'); for (const btn of nextBtns) { if (btn.offsetParent !== null) { // 可见 btn.click(); addLog('🔜 已点击下一节','green'); return; } } // 旧UI:找当前章节的下一个 const tabs = document.querySelectorAll('.posCatalog_select, .tabtags span[style*="cursor:pointer"]'); for (let i = 0; i < tabs.length; i++) { if (tabs[i].classList.contains('posCatalog_active') || tabs[i].parentElement.classList.contains('currents')) { // 找下一个未锁定未完成的标签 for (let j = i+1; j < tabs.length; j++) { const tab = tabs[j]; const txt = tab.textContent.trim(); if (txt.includes('lock') || txt.includes('锁定')) continue; if (txt.includes('finish') || txt.includes('完成')) continue; tab.click(); addLog(`🔜 切换到下一节: ${txt.slice(0,30)}`,'green'); return; } break; } } // 通过全局跳转变量 window.top.jump = true; addLog('🔜 触发全局跳转','green'); } catch(e) { addLog(`⚠️ 自动跳转失败: ${e.message}`,'orange'); } } // ======================== ====== 启动(重写-带页面路由) ============================ function init() { if (!document.body) return setTimeout(init,100); const context = detectPageContext(); addLog(`📋 页面类型: ${context}`,'#666'); // 成绩/结果页不执行任何操作 if (document.querySelector('.mark_answer,.mark_score,.resultNum,.score') && context !== 'WORK_PAGE') return; // 检查API配置 const needsConfig = !config.apiUrl || !config.apiKey || !config.model; // 先创建UI(各个页面都需要面板) if (needsConfig) { const t = setInterval(() => { if (document.querySelector('.questionLi,.TiMu') || context === 'COURSE_PAGE' || context === 'STUDY_PAGE') { clearInterval(t); createUI(); showFirstConfig(); updateStatus('⚠️ 请先配置 API 信息','orange'); } },500); setTimeout(() => clearInterval(t), 6000); return; } switch (context) { case 'COURSE_PAGE': createUI(); startMonitor(); startKeepAlive(); startFaceRecognitionCheck(); cxDecryptFont(); addLog('📋 课程页面 - 点击面板"开始刷课"自动处理视频+测验','#4caf50'); break; case 'WORK_PAGE': createUI(); cxDecryptFont(); addLog('📝 作业页面 - 自动答题','#4caf50'); // 直接用原答题流程 setTimeout(() => { if (config.autoAnswer) { preScanAnswered(); processAll(); } }, 2000); // 添加自动提交逻辑 if (config.autoSubmit) { setTimeout(() => { const btn = document.querySelector('.saveYl, [onclick*="save"], [onclick*="submit"], input[value="交卷"]'); if (btn && btn.offsetParent !== null) { addLog('📤 检测到交卷按钮','orange'); } }, 10000); } break; case 'STUDY_PAGE': startKeepAlive(); addLog('🎬 学习页面 - 开启背景保活','#4caf50'); createUI(); break; case 'ANSWER_PAGE': default: // 原答题逻辑 const t = setInterval(() => { if (document.querySelector('.questionLi,.TiMu,.topic_item')) { clearInterval(t); createUI(); startMonitor(); } },500); setTimeout(() => clearInterval(t), 6000); break; } } if (document.readyState === "complete" || document.readyState === "interactive") init(); else window.addEventListener('DOMContentLoaded', init); })();