// ==UserScript== // @name 【超星学习通助手】AI全自动答题(修复清空+强保护) // @namespace http://tampermonkey.net/ // @icon http://pan-yz.chaoxing.com/favicon.ico // @version 1.2.0 // @description 修复多选/判断/单选已作答被清空;预扫描持久缓存;每步保护不覆盖原有答案 // @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 // @connect api.muketool.com // @connect cx.lyck6.cn // @connect apione.apibyte.cn // @connect api.deepseek.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) }; // ======================== ====== 样式 ============================ 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} `); // ======================== ====== 首次配置弹窗 ============================ 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}题
`; 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; 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'); }; } 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}`); } // ======================== ====== 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(); 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); } // ======================== ====== 启动 ============================ function init() { if (!document.body) return setTimeout(init,100); if (document.querySelector('.mark_answer,.mark_score,.resultNum,.score')) return; // 首次使用检测:四项配置任意一项为空则弹出配置窗 const needsConfig = !config.apiUrl || !config.apiKey || !config.model; if (needsConfig) { const t = setInterval(() => { if (document.querySelector('.questionLi,.TiMu')) { clearInterval(t); createUI(); showFirstConfig(); updateStatus('⚠️ 请先配置 API 信息', 'orange'); } }, 500); setTimeout(() => clearInterval(t), 6000); return; } const t = setInterval(() => { if (document.querySelector('.questionLi,.TiMu')) { clearInterval(t); createUI(); startMonitor(); } }, 500); setTimeout(() => clearInterval(t), 6000); } if (document.readyState === "complete" || document.readyState === "interactive") init(); else window.addEventListener('DOMContentLoaded', init); })();