// ==UserScript==
// @name 中国宝武学习系统-全能版本学习助手【多账户、多模式】
// @namespace https://learn.baowugroup.com/
// @version 8.9.61
// @description 宝武学习平台全自动挂机(AI答题、自动考试、自动调查、自动收藏点赞评论、多账号托管)
// @author 令狐不太冲
// @icon https://learn.baowugroup.com/favicon.png
// @license MIT
// @match https://learn.baowugroup.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @require https://scriptcat.org/lib/8095/1.0.0/bw_learn_tool_utl_lib.js?sha384-8DUdixfmCvNg3ejsXviRkFfJm0drMFgmWTCOuEYiRa3Hgxr8W+Ct3J6BoKYtnKGp
// @require https://scriptcat.org/lib/8094/1.0.0/bw_api_lib.js?sha384-hcDDeNwBTdkm5rbC0Z4vcJJbgwAPqfJrUrYSFWt9J1gS0h7fkedEMabxFST8im8s
// @require https://cdn.jsdelivr.net/npm/tesseract.js@5.0.5/dist/tesseract.min.js
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
var BWAPI = unsafeWindow.BWAPI;
var BWUTL = unsafeWindow.BWUTL;
const STORE = { get: (k,d)=>GM_getValue(k,d), set: (k,v)=>GM_setValue(k,v) };
const S = {
running: STORE.get('bw_running', false),
queue: STORE.get('bw_queue', []),
queueIdx: STORE.get('bw_queueIdx', 0),
accounts: STORE.get('bw_accounts', []),
currentAccIdx: STORE.get('bw_currentAccIdx', 0),
zones: {}, tab: 'zone',
speed: STORE.get('bw_speed', 1.0),
scoreInterval: STORE.get('bw_scoreInterval', 30),
autoExam: STORE.get('bw_autoExam', true),
autoSurvey: STORE.get('bw_autoSurvey', true),
autoFav: STORE.get('bw_autoFav', true),
autoLike: STORE.get('bw_autoLike', true),
autoComment: STORE.get('bw_autoComment', true),
aiApiKey: STORE.get('bw_aiApiKey', ''),
logEl: null, studyTimer: null, chapterTimer: null, lastScoreCheck: 0,
retryCount: 0, doneBefore: STORE.get('bw_doneBefore', 0), doneToday: 0,
ocr: null, lastScore: null, lastStudyMinutes: -1, apiStaleCount: 0,
surveyHandled: false, examHandled: false,
};
const $ = (s,r=document)=>r.querySelector(s);
const $$ = (s,r=document)=>Array.from(r.querySelectorAll(s));
const sleep = ms=>new Promise(r=>setTimeout(r,ms));
const now = ()=>{const d=new Date();return `[${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}]`;};
function log(msg, type='info') {
const c = {info:'#4fc3f7',ok:'#81c784',warn:'#ffb74d',err:'#e57373',run:'#ce93d8'};
const line = document.createElement('div');
line.style.cssText = `color:${c[type]||'#ccc'};font-size:12px;line-height:1.6;`;
line.textContent = `${now()} ${msg}`;
if (S.logEl) { S.logEl.appendChild(line); S.logEl.scrollTop = S.logEl.scrollHeight; while (S.logEl.children.length>300) S.logEl.removeChild(S.logEl.firstChild); }
console.log(`[BW] ${msg}`);
}
function route() { const h = location.hash; if (h.includes('/auth/login')) return 'login'; if (h.includes('/zoneMore')) return 'zone'; if (h.includes('/courseStudy')) return 'study'; return 'home'; }
function getLoginName() { try { return JSON.parse(localStorage.getItem('userInfo')).loginName || null; } catch(e) { return null; } }
async function fetchZones(classType) { if (classType === 'OCE') return await BWAPI.fetchPublicCourses('', 1, 100); else return await BWAPI.fetchZoneList('', 1, 100, classType); }
async function fetchZoneCourses(zone) { return await BWAPI.fetchZoneCoursesDetail(zone.centerCode||'C001', zone.olClassNo||zone.guid, zone.olClassType||'ZE0'); }
function createPanel() {
if ($('#bw-helper')) return;
const p = document.createElement('div');
p.id = 'bw-helper';
p.innerHTML = `
宝武学习助手 v8.9.61
待机−
`;
document.body.appendChild(p);
S.logEl = $('#bw-log');
$('#bw-mn').onclick = (e)=>{e.stopPropagation(); p.classList.add('off'); p.style.left='auto'; p.style.top='auto'; p.style.right='16px'; p.style.bottom='16px';};
$('#bw-hdr').onclick = ()=>{ if(p.classList.contains('off')){ p.classList.remove('off'); } };
$('#bw-hdr').addEventListener('mousedown', e => {
if (p.classList.contains('off')) return;
let dx=e.clientX-p.offsetLeft, dy=e.clientY-p.offsetTop, dr=true;
const mv=ev=>{if(dr){p.style.left=(ev.clientX-dx)+'px';p.style.top=(ev.clientY-dy)+'px';p.style.right='auto';p.style.bottom='auto';}};
const up=()=>{dr=false;document.removeEventListener('mousemove',mv);document.removeEventListener('mouseup',up);};
document.addEventListener('mousemove',mv);document.addEventListener('mouseup',up);
});
$$('.tab').forEach(t=>t.onclick=()=>{$$('.tab').forEach(x=>x.classList.remove('on'));t.classList.add('on');S.tab=t.dataset.t;renderList();});
$('#bw-refresh').onclick = ()=>{ loadAllZones(true); };
$('#bw-all').onclick = ()=>$$('#bw-cl input[type=checkbox]').forEach(c=>{if(!c.closest('.ci').querySelector('.cs').classList.contains('done'))c.checked=true;});
$('#bw-go').onclick = startLearning;
$('#bw-stop').onclick = stopLearning;
$('#bw-speed').onchange = ()=>{S.speed=parseFloat($('#bw-speed').value)||1.0;STORE.set('bw_speed',S.speed);log('倍速'+S.speed+'x');};
$('#bw-score-interval').onchange = ()=>{S.scoreInterval=parseInt($('#bw-score-interval').value)||30;STORE.set('bw_scoreInterval',S.scoreInterval);log('检验间隔'+S.scoreInterval+'秒');};
$('#bw-auto-exam').onchange = (e)=>{S.autoExam=e.target.checked;STORE.set('bw_autoExam',S.autoExam);log('自动考试'+(S.autoExam?'开启':'关闭'));};
$('#bw-auto-survey').onchange = (e)=>{S.autoSurvey=e.target.checked;STORE.set('bw_autoSurvey',S.autoSurvey);log('自动调查'+(S.autoSurvey?'开启':'关闭'));};
$('#bw-auto-fav').onchange = (e)=>{S.autoFav=e.target.checked;STORE.set('bw_autoFav',S.autoFav);log('自动收藏'+(S.autoFav?'开启':'关闭'));};
$('#bw-auto-like').onchange = (e)=>{S.autoLike=e.target.checked;STORE.set('bw_autoLike',S.autoLike);log('自动点赞'+(S.autoLike?'开启':'关闭'));};
$('#bw-auto-comment').onchange = (e)=>{S.autoComment=e.target.checked;STORE.set('bw_autoComment',S.autoComment);log('自动评论'+(S.autoComment?'开启':'关闭'));};
$('#bw-add-acc').onclick = addAccount;
$('#bw-host').onclick = startHosting;
$('#bw-ai-apikey').onchange = async function() {
const input = this; const newValue = input.value.trim();
input.disabled = true; setTimeout(() => { input.disabled = false; }, 10000);
S.aiApiKey = newValue; STORE.set('bw_aiApiKey', S.aiApiKey);
log('AI API Key已保存,正在测试连接...', 'run');
if (!newValue) { log('AI API Key已清空,关闭AI答题', 'warn'); input.disabled = false; return; }
try { const testResult = await callAI('测试题目:1+1=?', ['1', '2', '3'], 'single'); if (testResult && testResult.length > 0) log('AI连接测试成功!答题可用', 'ok'); else log('AI连接测试失败,将使用默认答案', 'err'); }
catch (e) { log('AI连接测试失败: ' + e.message + ',将使用默认答案', 'err'); }
input.disabled = false;
};
$('#bw-ai-header').onclick = () => { $('#bw-ai-section').classList.toggle('collapsed'); };
$('#bw-acc-header').onclick = () => { $('#bw-acc-section').classList.toggle('collapsed'); };
renderAccList();
}
function setStatus(t,c){const e=$('#bw-st');if(e){e.textContent=t;e.style.color=c||'#f9e2af';}}
function setWorking(on){setStatus(on?'工作中':'待机',on?'#a6e3a1':'#f9e2af');}
function setProg(p){const b=$('#bw-pb');if(b)b.style.width=Math.min(100,Math.max(0,p))+'%';}
function isPassed(score) { if (!score || score.score == null) return false; return score.score >= (score.passLine || 60); }
function setScoreProg(score,passLine){
const b=$('#bw-pb-score'); const t=$('#bw-score-text'); const mt=$('#bw-min-score');
if(!b||!t)return;
if(score!=null&&passLine!=null){ const pct=Math.min(100,Math.max(0,score/passLine*100)); b.style.width=pct+'%'; t.textContent=`${score} / ${passLine} (${Math.round(pct)}%)`; b.style.background=pct>=100?'#81c784':'#f38ba8'; if(mt) mt.textContent = `(${score}/${passLine}(${Math.round(pct)}%))`; }
else { b.style.width='0%'; t.textContent='-- / --'; if(mt) mt.textContent = ''; }
}
function setProgressText(){ const e=$('#bw-prog-text'); if(e){ const currentAcc = getLoginName() || '未登录'; e.textContent=`当前学习账号: ${currentAcc}`; } }
function addAccount() {
const acc = $('#bw-acc').value.trim(); const pwd = $('#bw-pwd').value;
if (!acc || !pwd) { log('请输入工号和密码','warn'); return; }
const zones = $$('#bw-zones-check input:checked').map(c=>c.value);
let found = false;
for (let i = 0; i < S.accounts.length; i++) { if (S.accounts[i].acc === acc) { S.accounts[i] = {acc, pwd, zones}; found = true; break; } }
if (!found) S.accounts.push({acc, pwd, zones});
STORE.set('bw_accounts', S.accounts);
$('#bw-acc').value=''; $('#bw-pwd').value=''; renderAccList();
log(`账号${acc}已${found?'更新':'添加'} (${zones.join(',')})`,'ok');
}
function renderAccList() {
const el = $('#bw-acc-list'); if (!el) return; el.innerHTML = '';
S.accounts.forEach((a, i) => {
const d = document.createElement('div'); d.className = 'acc-item';
d.innerHTML = `${i+1}. ${a.acc}${a.zones.join('/')}编辑删除`;
d.querySelector('.del').onclick = ()=>{ S.accounts.splice(i,1); STORE.set('bw_accounts',S.accounts); renderAccList(); };
d.querySelector('.edit').onclick = ()=>{ $('#bw-acc').value = a.acc; $('#bw-pwd').value = a.pwd; $$('#bw-zones-check input').forEach(cb => { cb.checked = a.zones.includes(cb.value); }); log('编辑账号: ' + a.acc); };
el.appendChild(d);
});
const header = document.querySelector('#bw-acc-header span');
if (header) header.innerHTML = `账号托管 (${S.accounts.length}个账号)`;
}
const TAB_CLASS = {open:'OCE', zone:'ZE0', online:'ZE1', train:'TCE'};
const TAB_NAME = {open:'公开课', zone:'学习专区', online:'网络专题班', train:'培训班'};
async function loadAllZones(force) {
log('后台API读取各专区...','run'); setStatus('读取中','#89b4fa');
for (const [key, ct] of Object.entries(TAB_CLASS)) {
if (!force && S.zones[key]?.length) continue;
try { const list = await fetchZones(ct); S.zones[key] = list; log(`${TAB_NAME[key]}: ${list.length}个`); }
catch(e) { log(`${TAB_NAME[key]}读取失败: ${e.message}`,'err'); }
}
renderList(); if (!S.running) setWorking(false); log('读取完成','ok');
}
function renderList() {
const list = S.zones[S.tab] || []; const cl = $('#bw-cl'); cl.innerHTML = '';
if (!list.length) { cl.innerHTML = '暂无数据
'; return; }
list.forEach((item, i) => {
let st='', cls='todo';
let name = item.courseName || item.olClassName || item.name || item.guid;
if (item.isOpenCourse || item.courseGuid) { st = item.learnStatus==='2'?'已完成':'未学习'; cls = item.learnStatus==='2'?'done':'todo'; }
else { const total=parseInt(item.courseNum)||0, done=parseInt(item.learnNum)||0; if(total>0&&done>=total){st='已完成';cls='done';} else if(done>0){st=`学习中 ${done}/${total}`;cls='doing';} else{st='未学习';cls='todo';} }
const d=document.createElement('div'); d.className='ci';
d.innerHTML=`${name}${st}`;
cl.appendChild(d);
});
}
async function startHosting() { if (!S.accounts.length) { log('请先添加账号','warn'); return; } S.running = true; STORE.set('bw_running', true); S.currentAccIdx = 0; setWorking(true); log(`托管开始,共${S.accounts.length}个账号`,'run'); await hostNextAccount(); }
async function hostNextAccount() {
if (!S.running) return;
if (S.currentAccIdx >= S.accounts.length) { setWorking(false); log('所有账号托管完成!','ok'); STORE.set('bw_running', false); return; }
const acc = S.accounts[S.currentAccIdx];
log(`=== 账号 ${S.currentAccIdx+1}/${S.accounts.length}: ${acc.acc} ===`, 'run');
const curRoute = route();
if (curRoute === 'login') { log('当前已在登录页,直接登录'); await doLoginWithAccount(acc); }
else { const currentAcc = getLoginName(); log(`当前登录账号: ${currentAcc||'未知'},目标: ${acc.acc}`); if (currentAcc && String(currentAcc) === String(acc.acc)) { log('账号一致,跳过退出登录','ok'); await sleep(3000); } else { log(`账号不一致,需要切换`,'warn'); await logoutAndLogin(acc); await sleep(3000); } }
S.zones = {}; await loadAllZones(true);
let totalAll = 0, doneAll = 0; const todoCourses = [];
for (const zk of acc.zones) {
const list = S.zones[zk] || [];
for (const item of list) {
if (item.isOpenCourse || item.courseGuid) { totalAll++; if (item.learnStatus === '2') doneAll++; else todoCourses.push(item); }
else { const zoneName = item.courseName || item.olClassName || item.name || item.guid; log(`扫描: ${zoneName}...`); try { const courses = await fetchZoneCourses(item); courses.forEach(c => { totalAll++; if (c.learnStatus === '2') doneAll++; else todoCourses.push({...c, zoneName}); }); log(` ${zoneName}: 共${courses.length}门,已完成${courses.filter(c=>c.learnStatus==='2').length}门`); } catch(e) { log(`扫描${zoneName}失败`,'err'); } }
}
}
S.totalCourses = totalAll; S.doneBefore = doneAll; S.doneToday = 0;
STORE.set('bw_totalCourses', totalAll); STORE.set('bw_doneBefore', doneAll);
S.queue = todoCourses; S.queueIdx = 0; STORE.set('bw_queue', S.queue); STORE.set('bw_queueIdx', 0);
setProgressText(); log(`统计: 共${totalAll}门,已完成${doneAll}门,待学${todoCourses.length}门`,'ok');
if (todoCourses.length === 0) { log('无待学课程,切换下一账号','warn'); S.currentAccIdx++; STORE.set('bw_currentAccIdx', S.currentAccIdx); await sleep(2000); await hostNextAccount(); }
else await learnNext();
}
async function logoutAndLogin(acc) {
log('退出当前账号...','run');
const avatarBtn = document.querySelector('button.outline-none'); if (avatarBtn) { avatarBtn.click(); await sleep(2000); }
const items = document.querySelectorAll('.el-dropdown-menu__item, [role="menuitem"], .el-dropdown-menu li');
for (const item of items) { if (item.textContent.includes('退出登录') && item.offsetParent !== null) { item.click(); log('点了退出登录'); break; } }
await sleep(2000);
const confirmBtns = document.querySelectorAll('.el-message-box button, .el-dialog__footer button, .el-button--primary');
for (const btn of confirmBtns) { if (btn.offsetParent !== null && btn.textContent.trim().match(/确定|确认|退出/)) { btn.click(); log('确认退出弹窗: 点确定'); break; } }
await sleep(4000); location.hash = '#/auth/login'; await sleep(5000); await doLoginWithAccount(acc);
}
function solveMath(str) {
let expr = str.split(/[==??]/)[0]; expr = expr.replace(/[¥¥]/g, '+'); expr = expr.replace(/[^\d+\-×xX*÷/]/g, '');
const m = expr.match(/(\d+)\s*([+\-×xX*÷/])\s*(\d+)/);
if (!m) return null;
const a=parseInt(m[1]),op=m[2],b=parseInt(m[3]);
if(op==='+')return a+b; if(op==='-')return a-b; if(op==='×'||op==='x'||op==='X'||op==='*')return a*b; if(op==='÷'||op==='/')return b?Math.round(a/b):null; return null;
}
async function ocrCaptcha() {
const img = document.querySelector('img.captcha-image'); if (!img || !img.src) return null;
const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth * 5; canvas.height = img.naturalHeight * 5;
const ctx = canvas.getContext('2d'); ctx.fillStyle = 'white'; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const d = ctx.getImageData(0,0,canvas.width,canvas.height);
for (let i=0; i{}});
await S.ocr.setParameters({tessedit_pageseg_mode:'7'});
const {data} = await S.ocr.recognize(canvas.toDataURL()); return (data.text||'').trim();
}
async function doLoginWithAccount(acc) {
log('登录: ' + acc.acc, 'run');
let inputs = [];
for (let i=0; i<10; i++) { inputs = $$('input.el-input__inner'); if (inputs.length >= 3) break; await sleep(1000); }
if (inputs.length < 3) { log('登录页输入框未加载','err'); return; }
inputs[0].focus(); inputs[0].value = acc.acc; inputs[0].dispatchEvent(new Event('input',{bubbles:true})); inputs[0].dispatchEvent(new Event('change',{bubbles:true})); await sleep(300);
inputs[1].focus(); inputs[1].value = acc.pwd; inputs[1].dispatchEvent(new Event('input',{bubbles:true})); inputs[1].dispatchEvent(new Event('change',{bubbles:true})); await sleep(300);
for (let attempt = 1; attempt <= 15; attempt++) {
await sleep(1000); let text = '';
try { text = await ocrCaptcha(); } catch(e) { log('OCR异常: '+e.message,'err'); }
log(`验证码OCR(第${attempt}次): "${text}"`);
const ans = solveMath(text);
if (ans !== null) {
const capInput = $$('input.el-input__inner')[2];
if (capInput) { capInput.focus(); capInput.value = String(ans); capInput.dispatchEvent(new Event('input',{bubbles:true})); capInput.dispatchEvent(new Event('change',{bubbles:true})); log('填入验证码: ' + ans); }
await sleep(500);
const btns = Array.from(document.querySelectorAll('button'));
for (const b of btns) { if (b.textContent.trim() === '登录' && b.offsetParent !== null) { b.click(); break; } }
log('点了登录,等待响应...'); await sleep(4000);
if (!location.hash.includes('/auth/login')) { log('登录成功!','ok'); return; }
log('验证码错误,自动重试...','warn'); await sleep(1000);
} else { log('验证码识别失败,点击刷新','warn'); const img = document.querySelector('img.captcha-image'); if (img) img.click(); await sleep(1500); }
}
log('验证码重试15次仍未成功','err');
}
async function startLearning() {
const checked = $$('#bw-cl input[type=checkbox]:checked'); if (!checked.length) { log('请先勾选','warn'); return; }
setStatus('加载中','#89b4fa');
let totalAll = 0, doneAll = 0; const todoCourses = [];
for (const cb of checked) {
const i = parseInt(cb.dataset.i); const item = (S.zones[S.tab]||[])[i]; if (!item) continue;
if (item.isOpenCourse || item.courseGuid) { totalAll++; if (item.learnStatus === '2') doneAll++; else todoCourses.push(item); }
else { const zoneName = item.courseName || item.olClassName || item.name || item.guid; log(`扫描: ${zoneName}...`); try { const courses = await fetchZoneCourses(item); courses.forEach(c => { totalAll++; if (c.learnStatus === '2') doneAll++; else todoCourses.push({...c, zoneName}); }); log(` 共${courses.length}门,已完成${courses.filter(c=>c.learnStatus==='2').length}门`); } catch(e) { log(`扫描失败: ${e.message}`,'err'); } }
}
if (!todoCourses.length) { log('无未完成课程','warn'); setWorking(false); return; }
todoCourses.sort((a,b)=> (a.learnStatus==='1'?0:1)-(b.learnStatus==='1'?0:1));
S.queue = todoCourses; S.queueIdx = 0; S.running = true; S.totalCourses = totalAll; S.doneBefore = doneAll; S.doneToday = 0;
STORE.set('bw_running',true); STORE.set('bw_queue',S.queue); STORE.set('bw_queueIdx',0); STORE.set('bw_totalCourses',totalAll); STORE.set('bw_doneBefore',doneAll);
setProgressText(); setWorking(true); log(`开始学习,共${totalAll}门(已完成${doneAll}),待学${todoCourses.length}门`,'run'); learnNext();
}
function stopLearning() { S.running=false; STORE.set('bw_running',false); if(S.studyTimer)clearInterval(S.studyTimer); if(S.chapterTimer)clearInterval(S.chapterTimer); setWorking(false); log('已停止','warn'); }
async function learnNext() {
if (!S.running) return;
if (S.queueIdx >= S.queue.length) { const done = Math.min(S.doneBefore + S.doneToday, S.totalCourses); log(`本批次完成: ${done}/${S.totalCourses}门`,'ok'); if (S.accounts.length > 1 && S.currentAccIdx < S.accounts.length) { S.currentAccIdx++; STORE.set('bw_currentAccIdx',S.currentAccIdx); await sleep(2000); await hostNextAccount(); } else { setWorking(false); log('全部学习完毕!','ok'); STORE.set('bw_running',false); } return; }
const c = S.queue[S.queueIdx];
S.retryCount = 0; S.lastStudyMinutes = -1; S.apiStaleCount = 0;
S.surveyHandled = false; S.examHandled = false;
setScoreProg(null,null); setProgressText();
log(`=== ${S.queueIdx+1}/${S.queue.length}: ${c.courseName} ===`, 'run');
location.assign(`https://learn.baowugroup.com/#/courseStudy?centerCode=${c.centerCode}&guid=${c.guid}`);
}
function playCurrentVideo(video) {
if (!video || video.ended || !video.paused) return false;
const strategies = [ { sel: '.xgplayer-start', name: '中央播放按钮' }, { sel: '.xgplayer-play .xgplayer-icon', name: '控制栏播放图标' }, { sel: '#xg-video, .xgplayer', name: '视频画面区域' } ];
for (const { sel, name } of strategies) { const el = document.querySelector(sel); if (el && el.offsetParent) { el.click(); log(`点击${name}`); return true; } }
video.play().catch(()=>{}); log('兜底video.play()'); return true;
}
function cleanupVideoOverlays() { const start = document.querySelector('.xgplayer-start'); if (start) start.classList.add('hide'); const overlay = document.querySelector('.video-overlay'); if (overlay) overlay.style.display = 'none'; }
async function callAI(question, options, type) {
if (!S.aiApiKey) return null;
try {
const prompt = `你是一个考试答题专家。请根据以下题目和选项,给出正确答案。题目类型:${type === 'single' ? '单选题' : (type === 'multiple' ? '多选题' : '判断题')}
题目:${question}
选项:
${options.map((opt, i) => `${i + 1}. ${opt}`).join('\n')}
请只返回正确选项的编号,用逗号分隔,例如:1,2`;
const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + S.aiApiKey}, body: JSON.stringify({ model: 'deepseek-chat', messages: [ { role: 'system', content: '你是一个专业的考试答题助手。请只返回正确选项的编号,用逗号分隔。' }, { role: 'user', content: prompt } ], temperature: 0.1, max_tokens: 50 }) });
if (!response.ok) throw new Error('HTTP ' + response.status);
const data = await response.json(); const answer = data.choices[0].message.content.trim();
const nums = answer.match(/\d+/g) || []; const indices = nums.map(n => parseInt(n) - 1).filter(i => i >= 0 && i < options.length);
return indices.length > 0 ? indices : null;
} catch (e) { log('AI答题失败: ' + e.message + ',使用默认答案', 'warn'); return null; }
}
function getDefaultAnswer(options, type) { if (type === 'single' || type === 'judge') return [0]; if (type === 'multiple') return options.map((_, i) => i); return [0]; }
async function doExamQuestions() {
for (let retry = 0; retry < 10; retry++) {
const questionContainers = $$('.option-group');
if (questionContainers.length === 0) { log('没找到题目选项,第' + (retry+1) + '次重试...','warn'); await sleep(5000); continue; }
log(`共${questionContainers.length}道题,开始答题...`);
let answered = 0;
for (const group of questionContainers) {
let questionText = ''; const parent = group.parentElement;
if (parent) { const fullText = parent.innerText; const lines = fullText.split('\n').filter(l => l.trim().length > 0); if (lines.length > 0) questionText = lines[0].trim(); }
if (!questionText) questionText = '考试题目';
const radios = group.querySelectorAll('input[type="radio"]');
if (radios.length > 0) {
const options = Array.from(radios).map(r => r.parentElement.textContent.trim());
const type = radios.length === 2 && (radios[0].parentElement.textContent.includes('对') || radios[0].parentElement.textContent.includes('正确')) ? 'judge' : 'single';
let correctIndices = await callAI(questionText, options, type);
if (!correctIndices) { correctIndices = getDefaultAnswer(options, type); log(`默认答: ${questionText.slice(0, 30)}... -> 选项${correctIndices[0] + 1}`); }
else log(`AI答: ${questionText.slice(0, 30)}... -> 选项${correctIndices[0] + 1}`);
radios[correctIndices[0]].parentElement.click(); answered++;
}
const checkboxes = group.querySelectorAll('input[type="checkbox"]');
if (checkboxes.length > 0) {
const options = Array.from(checkboxes).map(cb => cb.parentElement.textContent.trim());
let correctIndices = await callAI(questionText, options, 'multiple');
if (!correctIndices) { correctIndices = getDefaultAnswer(options, 'multiple'); log(`默认多: ${questionText.slice(0, 30)}... -> 全选`); }
else log(`AI多: ${questionText.slice(0, 30)}... -> 选项${correctIndices.map(i => i + 1).join(',')}`);
correctIndices.forEach(i => { if (!checkboxes[i].checked) checkboxes[i].parentElement.click(); }); answered++;
}
}
log(`已答${answered}道题`); await sleep(2000);
const submitBtn = Array.from($$('button')).find(b => b.textContent.includes('我要交卷') && b.offsetParent);
if (!submitBtn) { log('没找到交卷按钮','warn'); break; }
submitBtn.click(); await sleep(2000);
const confirmSubmitBtn = Array.from($$('button')).find(b => b.textContent.includes('提交试卷') && b.offsetParent);
if (confirmSubmitBtn) { confirmSubmitBtn.click(); log('已提交试卷'); await sleep(8000); }
const pageText = document.body.innerText;
const scoreMatch = pageText.match(/得分[::]\s*(\d+\.?\d*)\s*分/);
const isPassMatch = pageText.match(/考试及格|考试合格|恭喜您.*及格|成绩.*合格/);
const isFailMatch = pageText.match(/考试不及格|未及格|成绩.*不合格|很遗憾/);
if (isPassMatch || (scoreMatch && parseFloat(scoreMatch[1]) >= 60)) { log(`考试及格!${scoreMatch ? scoreMatch[1] : ''}分`,'ok'); const exitBtn = Array.from($$('button')).find(b => b.textContent.includes('退出考试') && b.offsetParent); if (exitBtn) exitBtn.click(); await sleep(3000); return true; }
else if (isFailMatch || (scoreMatch && parseFloat(scoreMatch[1]) < 60)) { log(`考试不及格,得分${scoreMatch ? scoreMatch[1] : '?'}分,重新考`,'warn'); retry++; const againBtn = Array.from($$('button')).find(b => b.textContent.includes('重新考试') && b.offsetParent); if (againBtn) { againBtn.click(); await sleep(5000); } else { log('没找到重新考试按钮','warn'); break; } }
else { log('没识别到成绩,可能还在加载','warn'); await sleep(3000); retry++; }
}
return true;
}
async function handleExam() {
if (!S.autoExam) return true;
if (location.hash.includes('/examination') || location.hash.includes('/exam')) { log('已经在考试页面,直接答题...'); await doExamQuestions(); return true; }
await sleep(3000); log('查找页面上的未完成考试...','run');
const allElements = $$('div, .el-card, .card, .item'); let hasUnfinishedExam = false;
for (const el of allElements) {
if (!el.offsetParent) continue; const text = el.innerText || '';
if (text.includes('参与状态:未参加') || text.includes('参与状态: 未参加')) {
if (text.includes('考试') || text.includes('开始时间')) {
hasUnfinishedExam = true;
const enterBtn = Array.from(el.querySelectorAll('button')).find(b => b.textContent.includes('进入考试') || b.textContent.includes('开始考试'));
if (enterBtn && enterBtn.offsetParent) { log('发现未完成的考试,点击进入...'); enterBtn.click(); await sleep(3000); const confirmStartBtn = Array.from($$('button')).find(b => b.textContent.trim() === '确定' && b.offsetParent && b.closest('.el-dialog, .el-message-box')); if (confirmStartBtn) { log('点击考试弹窗的确定按钮'); confirmStartBtn.click(); await sleep(5000); } await doExamQuestions(); break; }
}
}
}
if (!hasUnfinishedExam) log('没有发现未完成的考试,跳过','ok'); log('考试完成检查完毕','info'); return true;
}
async function doSurveyQuestions() {
// 等待radio加载,最多重试6次(每次2秒)
let allRadios = [];
for (let waitRound = 0; waitRound < 6; waitRound++) {
await sleep(2000);
allRadios = $$('.el-radio');
if (allRadios.length > 0) break;
log(`调查题加载中,第${waitRound+1}次等待...`,'warn');
}
if (allRadios.length === 0) { log('调查页未找到选项,跳过','warn'); return true; }
// 自动检测每组选项数:通过第一个radio所在行的兄弟radio数量推断
let groupSize = 4;
const firstRow = allRadios[0].closest('.el-radio-group, .el-form-item__content, .survey-item, [class*="question"], [class*="item"]');
if (firstRow) {
const rowRadios = firstRow.querySelectorAll('.el-radio');
if (rowRadios.length >= 2) groupSize = rowRadios.length;
}
// 兜底:如果总radio数不能被4整除但能被3整除,按3处理
if (allRadios.length % 4 !== 0 && allRadios.length % 3 === 0) groupSize = 3;
const totalQuestions = Math.floor(allRadios.length / groupSize);
log(`共${totalQuestions}道调查题(每组${groupSize}个选项),开始填写...`);
// 选每组第0个选项(最左边=好/较好/合理)
for (let i = 0; i < allRadios.length; i += groupSize) {
if (allRadios[i] && allRadios[i].offsetParent) allRadios[i].click();
}
await sleep(2000);
let submitBtn = null;
const footer = document.querySelector('.footer-button');
if (footer) { const btns = footer.querySelectorAll('button'); for (const btn of btns) { if (btn.textContent.includes('提交') && btn.offsetParent) { submitBtn = btn; break; } } }
if (!submitBtn) { const btns = $$('.el-button'); for (const btn of btns) { if (btn.textContent.includes('提交') && btn.offsetParent) { submitBtn = btn; break; } } }
if (submitBtn) { log('点击提交按钮'); submitBtn.click(); await sleep(2000); const confirmBtn = Array.from($$('button')).find(b => b.textContent.includes('确定') && b.offsetParent && b.closest('.el-message-box, .el-dialog')); if (confirmBtn) { log('点击确认提交'); confirmBtn.click(); } await sleep(5000); log('调查已提交!','ok'); }
else log('没找到调查提交按钮','warn');
return true;
}
async function handleSurvey() {
if (!S.autoSurvey) return true;
if (S.surveyHandled) { log('本课程调查已处理过,跳过','info'); return true; }
if (location.hash.includes('/questionnaire') || location.hash.includes('/survey')) { log('已经在调查页面,直接填写...'); await doSurveyQuestions(); S.surveyHandled = true; return true; }
await sleep(2000); log('查找页面上的未完成调查...','run');
const allElements = $$('div, .el-card, .card, .item'); let hasUnfinishedSurvey = false;
for (const el of allElements) {
if (!el.offsetParent) continue; const text = el.innerText || '';
if (text.includes('参与状态:未参与') || text.includes('参与状态: 未参与')) {
if (text.includes('调查') || text.includes('问卷') || text.includes('开始时间')) {
hasUnfinishedSurvey = true;
const enterBtn = Array.from(el.querySelectorAll('button')).find(b => b.textContent.includes('进入调查') || b.textContent.includes('开始调查'));
if (enterBtn && enterBtn.offsetParent) { log('发现未完成的调查,点击进入...'); enterBtn.click(); await sleep(5000); await doSurveyQuestions(); S.surveyHandled = true; break; }
}
}
}
if (!hasUnfinishedSurvey) log('没有发现未完成的调查,跳过','ok'); log('调查完成检查完毕','info'); return true;
}
const COMMENT_TEXTS = ['讲解清晰易懂,重点突出,收获很大。','课程内容扎实,逻辑严谨,学起来很顺畅。','老师讲得生动有趣,枯燥知识也变得好懂。','视频画质清晰,声音洪亮,学习体验很好。','节奏把控得当,难点讲解透彻,非常实用。','案例丰富贴近实际,帮助理解抽象概念。','知识点梳理系统,条理分明,便于记忆。','语言简洁明了,没有废话,效率很高。','互动设计巧妙,引人思考,不只是灌输。','每节课都有小结,重点复习很方便。','实操演示到位,步骤清楚,跟着做就会。','理论结合实践,讲解深入浅出,受益匪浅。','老师态度认真,答疑及时,很有责任心。','课程结构合理,由浅入深,循序渐进。','字幕准确同步,方便回看和做笔记。','内容更新及时,紧跟行业发展趋势。','讲解耐心细致,零基础也能听懂。','举例生动形象,复杂问题简单化。','视频时长适中,碎片时间也能学习。','重点难点反复强调,记忆更深刻。','课件制作精美,图文并茂,辅助效果好。','老师声音好听,语速适中,听着不累。','课程配套资料齐全,复习巩固很方便。','每章都有练习,检验学习效果很实用。','讲解风格幽默,学习过程轻松愉快。','逻辑链条完整,前后呼应,没有断层。','针对考点讲解精准,备考效率高。','实际操作环节演示清晰,上手很快。','老师经验丰富,分享很多干货技巧。','课程设计科学,符合认知学习规律。'];
async function autoLikeFavComment() {
if (!S.autoFav && !S.autoLike && !S.autoComment) return;
log('开始自动收藏、点赞、评论...','run');
if (S.autoFav) { const favBtn = Array.from(document.querySelectorAll('button.el-button')).find(b => b.textContent.includes('收藏') && b.offsetParent); if (favBtn) { favBtn.click(); log('已点击课程页收藏按钮'); await sleep(500); } }
if (S.autoLike) { const likeBtn = document.querySelector('button.like-button'); if (likeBtn) { likeBtn.click(); log('已点击课程页点赞按钮'); await sleep(500); } }
if (S.autoComment) {
const commentTab = Array.from($$('.tab-item button')).find(b => b.textContent.trim().includes('交流'));
if (commentTab) commentTab.click(); await sleep(1000);
const textarea = document.querySelector('.talk-input-wrapper textarea');
if (textarea) { const randomComment = COMMENT_TEXTS[Math.floor(Math.random() * COMMENT_TEXTS.length)]; textarea.focus(); textarea.value = randomComment; textarea.dispatchEvent(new Event('input', {bubbles: true})); await sleep(500); const submitBtn = Array.from(document.querySelectorAll('.talk-input-wrapper button')).find(b => b.textContent.includes('发表评论')); if (submitBtn) { submitBtn.click(); log('已发表评论'); await sleep(1000); } }
}
log('收藏点赞评论完成','ok');
}
async function studyCourse() {
setProgressText(); await sleep(5000);
await handleExam(); await handleSurvey(); await autoLikeFavComment();
const allTabs = $$('.el-tabs__item, .tab-item, [role="tab"]').filter(t => t.offsetParent);
const videoTab = allTabs.find(t => t.textContent.trim().includes('视频') || t.textContent.trim().includes('播放')) || allTabs[0];
if (videoTab) videoTab.click();
await sleep(3000);
let v = null;
for (let i=0;i<30;i++){ v=$('#xg-video video')||$('video'); if(v&&v.readyState>=1&&v.duration>0)break; await sleep(1000); }
if (!v||!v.duration) { log('视频加载失败,重试','err'); setTimeout(studyCourse, 5000); return; }
v.muted = true; v.playbackRate = S.speed; v.play().catch(()=>{});
await sleep(1500); playCurrentVideo(v); await sleep(2000);
const score = await checkScore();
if (score) { S.lastScore = score; setScoreProg(score.score, score.passLine); log(`初始: 成绩${score.score??'--'}/${score.passLine||60}分 (${isPassed(score)?'已及格':'进行中'})`); if (isPassed(score)) { log(`已及格:${score.score}分`,'ok'); S.queueIdx++; S.doneToday++; setProgressText(); STORE.set('bw_queueIdx',S.queueIdx); await sleep(2000); await learnNext(); return; } }
await playChapter(0);
S.studyTimer = setInterval(async ()=>{
if(!S.running){clearInterval(S.studyTimer);return;}
handlePopups(); watchdog();
const vv=$('#xg-video video')||$('video'); if(vv&&vv.duration) setProg(vv.currentTime/vv.duration*100);
const now=Date.now();
if(now-S.lastScoreCheck>S.scoreInterval*1000){ S.lastScoreCheck=now; const s=await checkScore(); if(s){ S.lastScore=s; setScoreProg(s.score, s.passLine); if(isPassed(s)){ log(`及格!${s.score}分/${s.passLine}分`,'ok'); clearInterval(S.studyTimer); S.queueIdx++; S.doneToday++; setProgressText(); STORE.set('bw_queueIdx',S.queueIdx); await sleep(2000); await learnNext(); } else log(`进度: 成绩${s.score??'--'}/${s.passLine||60}分`); } }
}, 3000);
}
async function playChapter(idx) {
if (!S.running) return;
const leaves = $$('.course-chapter').filter(el=>!el.classList.contains('chapter-parent'));
if (!leaves.length) { const v=$('#xg-video video')||$('video'); if(v){v.muted=true;v.playbackRate=S.speed;v.play().catch(()=>{});} if(S.chapterTimer)clearInterval(S.chapterTimer); S.chapterTimer=setInterval(()=>{ const vv=$('#xg-video video')||$('video'); if(!vv)return; if(vv.muted!==true)vv.muted=true; if(Math.abs(vv.playbackRate-S.speed)>0.1)vv.playbackRate=S.speed; if(vv.paused&&!vv.ended){playCurrentVideo(vv);} if(vv.ended||(vv.duration>0&&vv.currentTime>=vv.duration-0.5)){clearInterval(S.chapterTimer);onVideoEnded();} },2000); return; }
if (idx >= leaves.length) { onVideoEnded(); return; }
const ch = leaves[idx]; ch.scrollIntoView({block:'center'}); ch.click();
log(`小节${idx+1}/${leaves.length}: ${ch.textContent.trim().slice(0,40)}`);
await sleep(3000); const v=$('#xg-video video')||$('video'); if(v){v.muted=true;v.playbackRate=S.speed;playCurrentVideo(v);}
if(S.chapterTimer)clearInterval(S.chapterTimer);
S.chapterTimer=setInterval(()=>{ const vv=$('#xg-video video')||$('video'); if(!vv)return; if(vv.muted!==true)vv.muted=true; if(Math.abs(vv.playbackRate-S.speed)>0.1)vv.playbackRate=S.speed; if(vv.paused&&!vv.ended){playCurrentVideo(vv);} if(vv.ended||(vv.duration>0&&vv.currentTime>=vv.duration-0.5)){ clearInterval(S.chapterTimer); log(`小节${idx+1}完毕`); setTimeout(()=>playChapter(idx+1),2000); } },2000);
}
async function onVideoEnded() {
log('全部小节播完,检查成绩...','run'); await sleep(2000);
const s = await checkScore(); if (s) { S.lastScore=s; setScoreProg(s.score,s.passLine); }
if (isPassed(s)) { log(`及格!${s.score}分`,'ok'); S.queueIdx++; S.doneToday++; setProgressText(); STORE.set('bw_queueIdx',S.queueIdx); await sleep(2000); await learnNext(); }
else { S.retryCount++; log(`未及格(${s?s.score:'?'}分/${s?s.passLine:60}), 第${S.retryCount}次重播`,'warn'); if (S.retryCount >= 5) { log('重播5次仍未及格,跳下一门','err'); S.queueIdx++; STORE.set('bw_queueIdx',S.queueIdx); await sleep(2000); await learnNext(); } else setTimeout(()=>playChapter(0), 3000); }
}
function watchdog(){ const v=$('#xg-video video')||$('video'); if(!v)return; if(v.muted!==true)v.muted=true; if(Math.abs(v.playbackRate-S.speed)>0.1)v.playbackRate=S.speed; if(v.paused&&!v.ended){playCurrentVideo(v);} if(!v.paused) cleanupVideoOverlays(); }
async function checkScore() { return await checkScoreFromDOM(); }
async function checkScoreFromDOM() {
const btn = Array.from($$('button')).find(b=>b.textContent.trim()==='完成情况' && b.offsetParent); if (!btn) return null;
btn.click(); await sleep(5000); const dlg = $('.el-dialog'); if (!dlg) return null;
const t = dlg.innerText;
const pm1 = t.match(/合格分数线[::]\s*(\d+)/); const pm2 = t.match(/及格分数[::]\s*(\d+\.?\d*)/); const passLine = pm1 ? parseFloat(pm1[1]) : (pm2 ? parseFloat(pm2[1]) : 60);
const sm1 = t.match(/课程成绩[::]\s*([\d.]+)\s*分/); const sm2 = t.match(/总分[::]\s*([\d.]+)\s*分/); const sm3 = t.match(/最终成绩[::]\s*([\d.]+)\s*分/); const score = sm1 ? parseFloat(sm1[1]) : (sm2 ? parseFloat(sm2[1]) : (sm3 ? parseFloat(sm3[1]) : null));
const close = dlg.querySelector('.el-dialog__close, [aria-label="Close"]'); if (close) close.click(); else document.body.click(); await sleep(500);
return {score, passLine};
}
function handlePopups(){
$$('.el-message-box, .el-dialog, [class*="popup"], [class*="modal"]').forEach(el=>{
const r=el.getBoundingClientRect(); if(r.width>100&&el.offsetParent!==null){ const txt=el.textContent; if(txt.includes('休息')||txt.includes('专注学习')){ log('休息弹窗,点继续','warn'); const btns=el.querySelectorAll('button'); for(const b of btns){ const bt=b.textContent.trim(); if(bt==='继续'||bt==='确定'||bt==='知道了'||bt==='确认'){b.click();break;} } } }
});
const nextBtn = document.querySelector('.play-chapter button.el-button--primary'); if (nextBtn && nextBtn.offsetParent && !nextBtn.disabled && nextBtn.textContent.includes('播放下一节')) { log('自动点了: 播放下一节'); nextBtn.click(); return; }
}
async function boot(){
createPanel(); setProgressText(); setWorking(S.running);
log('宝武学习助手 v8.9.61 启动','ok');
const r=route();
if (location.hash.includes('/questionnaire') || location.hash.includes('/survey')) { log('检测到调查页面,自动填写...'); await doSurveyQuestions(); return; }
if (location.hash.includes('/examination') || location.hash.includes('/exam')) { log('检测到考试页面,自动答题...'); await doExamQuestions(); return; }
if(r==='login'){setStatus('登录中','#f38ba8');}
if(!S.zones.open || S.zones.open.length===0) loadAllZones(false);
if(r==='study' && S.running) await studyCourse();
setInterval(()=>{handlePopups();if(S.running&&route()==='study')watchdog();},2000);
window.addEventListener('hashchange',()=>{ setTimeout(async()=>{ if(S.running&&route()==='study')await studyCourse(); if(S.running&&route()==='home'&&S.queue[S.queueIdx]){ const c=S.queue[S.queueIdx]; location.assign(`https://learn.baowugroup.com/#/courseStudy?centerCode=${c.centerCode}&guid=${c.guid}`); } },3000); });
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot);
else boot();
})();