// ==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 = `
`;
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 = `
`;
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);
})();