// ==UserScript==
// @name 【超星学习通】免费题库+AI答题|视频挂机|章节测验|考试自动作答|字体解密
// @namespace http://tampermonkey.net/
// @icon http://pan-yz.chaoxing.com/favicon.ico
// @version 2.1.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;
let answeredCache = new Set(
(GM_getValue(CACHE_KEY, "") || "").split(",").filter(Boolean)
);
// 全局定时器池,统一销毁
const _timers = { intervals: [], timeouts: [] };
function _addTimer(type, id) { if (id) _timers[type].push(id); }
function _clearAllTimers() {
_timers.intervals.forEach(clearInterval);
_timers.timeouts.forEach(clearTimeout);
_timers.intervals.length = 0; _timers.timeouts.length = 0;
}
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 = `
`;
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;
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}`);
}
// ======================== ====== 答案记录弹窗 ============================
// ======================== ====== iframe安全读取 ============================
function getIframeDoc(iframe) {
try {
if (iframe && iframe.contentDocument) return iframe.contentDocument;
if (iframe && iframe.contentWindow) return iframe.contentWindow.document;
} catch(e) { /* 跨域忽略 */ }
return null;
}
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';
const total = records.length;
const pageSize = 50;
let html = '';
html += '
';
html += '
📝 答案记录';
html += '🗑 清空';
html += '×
';
records.slice(0, pageSize).forEach((r, i) => {
html += `
`;
html += `
${r.time || ''} | ${r.source || ''}
`;
html += `
${(r.question || '').slice(0,80)}
`;
html += `
${(r.answer || '').slice(0,120)}
`;
});
if (total > pageSize) html += `
仅显示最近${pageSize}条,共${total}条
`;
html += '
';
overlay.innerHTML = html;
document.body.appendChild(overlay);
document.getElementById('ai-records-close').onclick = () => overlay.remove();
document.getElementById('ai-records-clear').onclick = () => {
GM_setValue(ANSWER_RECORDS_KEY, '[]');
addLog('🗑 答案记录已清空','#9c27b0');
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) ============================
// 超星使用自定义字体加密文字,需解密后才能正确匹配答案
let _fontDecrypted = false;
function cxDecryptFont(iframeDocument) {
if (_fontDecrypted) return; // 单页面仅解密一次
try {
const doc = iframeDocument || document;
const els = doc.querySelectorAll('.font-cxsecret');
if (!els.length) return;
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;
_fontDecrypted = true;
const fontArr = base64ToUint8Array(fontMatch[1]);
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];
}
// 文本节点替换,不改动外层DOM结构
els.forEach(el => {
const walk = document.createTreeWalker(el, 4, null, false);
let node;
while (node = walk.nextNode()) {
let txt = node.nodeValue;
for (const [key, val] of Object.entries(match)) {
txt = txt.replace(new RegExp(String.fromCharCode(Number(key)), 'g'), String.fromCharCode(val));
}
node.nodeValue = txt;
}
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, retryCount) {
if (retryCount === undefined) retryCount = 1;
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:()=>{ if (retryCount > 0) { resolve(askAI(prompt, retryCount - 1)); } else { reject('网络异常'); } },
ontimeout:()=>{ if (retryCount > 0) { resolve(askAI(prompt, retryCount - 1)); } else { 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');
}
});
}, 1000);
}
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;
// 页面卸载时清理所有监听和定时器
window.addEventListener('beforeunload', function() {
if (observer) { observer.disconnect(); observer = null; }
if (cx_faceCheckTimer) { clearInterval(cx_faceCheckTimer); cx_faceCheckTimer = null; }
_clearAllTimers();
});
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) {
const submitBtn = idoc.querySelector('#videoquiz-submit, .video-quiz-submit, [onclick*="submitVideoQuiz"]');
if (submitBtn) submitBtn.click();
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 {
// 新版/旧版导航按钮
const navSelectors = '.next, .nextChapter, .orientationright, .jb_btn.jb_btn_92.fs14.prev_next.next, [class*="nextChapter"], .next-btn, .el-button--next';
const nextBtns = document.querySelectorAll(navSelectors);
for (const btn of nextBtns) {
if (btn.offsetParent !== null) {
btn.click();
addLog('🔜 已点击下一节','green');
return;
}
}
// 新版课程目录(Element UI / 新版超星)
const newTabs = document.querySelectorAll('.catalog-item:not(.disabled):not(.finished), .chapter-item:not(.lock):not(.finish), .section-item:not(.lock), [class*="catalog"]:not(.lock)');
for (const tab of newTabs) {
if (tab.classList.contains('active') || tab.classList.contains('current')) {
const next = tab.nextElementSibling;
if (next) { next.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);
})();