// ==UserScript==
// @name 高校教务系统智能抢课工具 v3.1
// @namespace https://github.com/school-course-grabber
// @version 3.1.0
// @description 【多课程版】支持一次抢多门课,按顺序依次监控。API嗅探+直连+预连接+自适应间隔。
// @author Auto-Grabber
// @license MIT
// @match *://*/student/courseSelect/*
// @match *://*/student/course/*
// @match *://*/student/selectCourse/*
// @match *://*/xsxk/*
// @match *://*/xk/*
// @match *://*/*courseSelect*
// @match *://*/*selectCourse*
// @match *://*/*elective*
// @include http://111.43.36.164/*
// @include http://172.20.139.153:7700/*
// @include https://*-*-*-*-*.webvpn.qqhru.edu.cn/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// ==================== 配置 ====================
const CONFIG = {
cardSelectors: [
'.course-card','.course-item','.courseItem','.kccg_item',
'div[class*="course" i]','div[class*="kccg" i]',
'tr[class*="course" i]','li[class*="course" i]',
'.card','div[class*="card" i]',
],
selectButtonSelectors: [
'button:contains("选择")','a:contains("选择")',
'button[class*="select" i]','a[class*="select" i]',
'.select-btn','.btn-select','button:contains("选课")','a:contains("选课")',
],
confirmButtonSelectors: [
'.modal button:contains("确定")','.modal button:contains("提交")',
'.modal button:contains("确认")','.el-dialog button:contains("确定")',
'.layui-layer button:contains("确定")','.ant-modal button:contains("确定")',
'button.confirm','button.submit',
],
domPollMin:500, domPollMax:800,
apiSafetyFloor:80, apiMargin:3,
playSound:true,
showDesktopNotification:true,
stopOnSuccess:false, // 多课程模式下不自动停止,抢完所有才停
debug:false,
};
// ==================== 状态 ====================
const STATE = {
targetList: [], // [{code, name, status:'pending'|'grabbed'|'failed'}]
currentIdx: 0, // 当前正在监控的课程在targetList中的索引
maxGrab: 1, // 最多抢几门
isRunning: false, timerId: null,
attemptCount: 0, grabbedCount: 0,
logLines: [],
apiMode: false,
apiTemplate: null,
capturedAPIs: [],
rttHistory: [],
cardSelector: null, matchedCardFound: false,
};
// ==================== 预连接 ====================
function injectPreconnect() {
const host = location.hostname;
if (!host || host === 'localhost' || host === '127.0.0.1') return;
[{rel:'dns-prefetch',href:'//'+host},{rel:'preconnect',href:'//'+host},{rel:'preconnect',href:'//'+host,crossorigin:'use-credentials'}].forEach(a=>{
const l=document.createElement('link');l.rel=a.rel;l.href=a.href;
if(a.crossorigin)l.setAttribute('crossorigin',a.crossorigin);
document.head.appendChild(l);
});
log('INFO','已注入预连接: '+host);
}
// ==================== API嗅探器 ====================
function setupAPISniffer() {
const KW=['select','course','elective','choose','enroll','pick','选课','选择','选','xk','xsxk','add','submit','save','commit'];
const isAPI=(u,b)=>KW.some(k=>(u+' '+(b||'')).toLowerCase().includes(k));
function norm(u,m,b){
try{new URL(u,location.origin)}catch(e){return null}
const p={};new URL(u,location.origin).searchParams.forEach((v,k)=>{p[k]=/^\d{3,}$/.test(v)?'{{INPUT}}':v});
const nu=new URL(u,location.origin);nu.search='';Object.entries(p).forEach(([k,v])=>nu.searchParams.set(k,v));
let bp=null;
if(b&&typeof b==='string'){try{const j=JSON.parse(b);bp=JSON.stringify(j).replace(/:\s*"\d{3,}"|:\s*\d{3,}/g,': "{{INPUT}}"')}catch(e){bp=b.replace(/\b\d{3,}\b/g,'{{INPUT}}')}}
return{url:nu.href,method:m.toUpperCase(),headers:{'Content-Type':'application/x-www-form-urlencoded'},bodyPattern:bp,rawBody:b};
}
const OX=window.XMLHttpRequest;
window.XMLHttpRequest=function(){const x=new OX();let m='',u='',b=null;
const oo=x.open;x.open=function(me,ur){m=me;u=ur;return oo.apply(this,arguments)};
const os=x.send;x.send=function(bd){b=bd;if(isAPI(u,b)){const t=norm(u,m,b);if(t&&!STATE.capturedAPIs.find(a=>a.url===t.url)){STATE.capturedAPIs.push(t);log('INFO','[嗅探] '+m+' '+u?.substring(0,80));updateAPIPanel()}}return os.apply(this,arguments)};
return x};
window.XMLHttpRequest.prototype=OX.prototype;
const of=window.fetch;window.fetch=function(i,ni){const u=typeof i==='string'?i:(i.url||'');const m=((ni||{}).method||'GET').toUpperCase();const b=(ni||{}).body||'';
if(isAPI(u,b)){const t=norm(u,m,typeof b==='string'?b:'');if(t&&!STATE.capturedAPIs.find(a=>a.url===t.url)){STATE.capturedAPIs.push(t);log('INFO','[嗅探] '+m+' '+u?.substring(0,80));updateAPIPanel()}}return of.apply(this,arguments)};
}
// ==================== 工具函数 ====================
function log(level,msg){
const ts=new Date().toLocaleTimeString(),line=`[${ts}] [${level}] ${msg}`;
console.log('[抢课] '+line);STATE.logLines.push(line);if(STATE.logLines.length>300)STATE.logLines.shift();
const box=document.getElementById('gb-log');
if(box){const d=document.createElement('div');d.textContent=line;d.style.cssText=`font-size:11px;line-height:1.5;font-family:'Courier New',monospace;color:${level==='SUCCESS'?'#0f0':level==='ERROR'?'#f44':level==='WARN'?'#fa0':level==='ALERT'?'#f44':level==='API'?'#0af':level==='PROGRESS'?'#ff0':'#0c6'};${level==='ALERT'?'font-weight:bold;':''}`;box.appendChild(d);box.scrollTop=box.scrollHeight}
const st=document.getElementById('gb-status-text'),sd=document.getElementById('gb-status-dot');
if(st){if(level==='STATUS'){st.textContent=msg;if(sd)sd.style.background='#fa0'}else if(level==='SUCCESS'){st.textContent='✅ '+msg;if(sd)sd.style.background='#0f0'}else if(level==='ALERT'){st.textContent='🚨 '+msg;if(sd)sd.style.background='#f44'}}
}
function playSound(){if(!CONFIG.playSound)return;try{const c=new(window.AudioContext||window.webkitAudioContext)();[800,1000,1200].forEach((f,i)=>{const o=c.createOscillator(),g=c.createGain();o.type='square';o.frequency.value=f;g.gain.setValueAtTime(.3,c.currentTime+i*.15);g.gain.exponentialRampToValueAtTime(.001,c.currentTime+i*.15+.12);o.connect(g);g.connect(c.destination);o.start(c.currentTime+i*.15);o.stop(c.currentTime+i*.15+.12)})}catch(e){}}
function notify(title,body){if(!CONFIG.showDesktopNotification)return;if('Notification' in window){if(Notification.permission==='granted')new Notification(title,{body});else if(Notification.permission==='default')Notification.requestPermission().then(p=>{if(p==='granted')new Notification(title,{body})})}}
function showPageAlert(title,msg,ok){const old=document.getElementById('gb-alert');if(old)old.remove();const ov=document.createElement('div');ov.id='gb-alert';ov.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:99999;display:flex;align-items:center;justify-content:center;';const cl=ok?'#00ff88':'#ff4444';ov.innerHTML=`
${ok?'🎉':'⚠️'}
${title}
${msg}
`;document.body.appendChild(ov);ov.addEventListener('click',e=>{if(e.target===ov)ov.remove()})}
// ==================== 进度辅助 ====================
function getCurrentTarget(){
if(STATE.currentIdx>=STATE.targetList.length)return null;
// 跳过非pending
while(STATE.currentIdx=STATE.targetList.length)return null;
return STATE.targetList[STATE.currentIdx];
}
function advanceToNext(){
STATE.currentIdx++;
STATE.matchedCardFound=false;
while(STATE.currentIdx=STATE.targetList.length)return false;
const t=STATE.targetList[STATE.currentIdx];
log('PROGRESS',`━━━ 切换到下一门: [${t.code||t.name}] (进度 ${STATE.grabbedCount}/${STATE.maxGrab}) ━━━`);
return true;
}
function countPending(){return STATE.targetList.filter(t=>t.status==='pending').length}
function allGrabbed(){return STATE.grabbedCount>=STATE.maxGrab||countPending()===0}
// ==================== API直连抢课 ====================
async function apiGrab(){
STATE.attemptCount++;
const target=getCurrentTarget();
if(!target){log('SUCCESS','✅ 所有课程抢课完成!');stopGrabber();return;}
if(!STATE.apiTemplate){log('ERROR','API模板为空,回退DOM模式');STATE.apiMode=false;return}
const tpl=STATE.apiTemplate;
const code=target.code||target.name;
let url=tpl.url.replace(/\{\{INPUT\}\}/g,code);
let body=(tpl.bodyPattern||'').replace(/\{\{INPUT\}\}/g,code);
try{
const init={method:tpl.method,credentials:'include',headers:Object.assign({},tpl.headers||{})};
if(tpl.method!=='GET'&&body)init.body=body;
const st=performance.now();
const resp=await fetch(url,init);
const elapsed=(performance.now()-st).toFixed(0);
const text=await resp.text();
STATE.rttHistory.push(+elapsed);
if(STATE.rttHistory.length>5)STATE.rttHistory.shift();
log('API',`[API直连] ${tpl.method} 响应 ${resp.status} (${elapsed}ms)`);
if(checkSuccess(resp,text)){
log('SUCCESS',`✅ 抢课成功: [${target.code||target.name}]`);
onGrabSuccess(target,'API直连');
}else if(/满|已满|full|no.*slot|无.*名额/i.test(text)){
log('DATA',`[${target.code||target.name}] 名额已满,继续监控...`);
}else if(/冲突|已选|已经|already|exist/i.test(text)){
log('SUCCESS',`可能已选过: [${target.code||target.name}]`);
onGrabSuccess(target,'API直连(可能已选)');
}else{log('WARN',`API返回未知状态: ${text.substring(0,150)}`)}
}catch(e){
log('ERROR',`API请求失败: ${e.message}`);
if(STATE.attemptCount>3){log('WARN','API连续失败,切换到DOM模式');STATE.apiMode=false}
}
}
function checkSuccess(resp,text){if(resp.ok){const fk=['失败','错误','异常','error','fail','exception','满'];if(!fk.some(k=>text.toLowerCase().includes(k)))return true}return false}
function onGrabSuccess(target,mode){
target.status='grabbed';
STATE.grabbedCount++;
const label=`[${target.code||target.name}]`;
playSound();setTimeout(()=>playSound(),400);
notify('🚨 抢课成功!',`课程 ${label} 已通过${mode}完成选课!(${STATE.grabbedCount}/${STATE.maxGrab})`);
showPageAlert('抢课成功!',`课程:${label}\n模式:${mode}\n进度:${STATE.grabbedCount}/${STATE.maxGrab}`,true);
if(allGrabbed()){
log('SUCCESS',`🎉 全部完成!共抢到 ${STATE.grabbedCount} 门课`);
stopGrabber();
}else if(!advanceToNext()){
log('SUCCESS','所有课程已处理完毕');
stopGrabber();
}
}
// ==================== DOM模式抢课 ====================
function findCards(){
if(STATE.cardSelector){const c=document.querySelectorAll(STATE.cardSelector);if(c.length>0)return Array.from(c)}
for(const s of CONFIG.cardSelectors){try{const c=document.querySelectorAll(s);if(c.length>0){STATE.cardSelector=s;return Array.from(c)}}catch(e){}}
const all=document.querySelectorAll('div,tr,li'),candidates=[];
for(const e of all){const t=e.textContent||'';if(/\b\d{2,4}\b/.test(t)&&/\d+\s*\/\s*\d+/.test(t)&&e.children.length>=2){let anc=false,p=e.parentElement;while(p&&p!==document.body){if(candidates.includes(p)){anc=true;break}p=p.parentElement}if(!anc)candidates.push(e)}}
return candidates
}
function extractCode(card){const t=(card.textContent||'').replace(/\s+/g,' ').trim();return(t.match(/\[(\d{2,4})\s*[-–—]/)||t.match(/\b(\d{2,4})\s*[-–—]/)||[''])[1]||''}
function extractName(card){const t=(card.textContent||'').replace(/\s+/g,' ').trim();const m=t.match(/\[?\d{2,4}\s*[-–—]\s*([^\s\]\[]+)/);return m?m[1]:''}
function parseCapacity(card){const t=(card.textContent||'').replace(/\s+/g,' ').trim();let m=t.match(/已选[\/\s]*容量[::]\s*(\d+)\s*\/\s*(\d+)/);if(m)return{sel:+m[1],cap:+m[2]};m=t.match(/已选[::]\s*(\d+)[\s\S]*?容量[::]\s*(\d+)/);if(m)return{sel:+m[1],cap:+m[2]};for(const mm of t.matchAll(/(\d+)\s*\/\s*(\d+)/g)){const s=+mm[1],c=+mm[2];if(c>=10&&c<=500&&s<=c)return{sel:s,cap:c}}return null}
function findSelectBtn(card){for(const s of CONFIG.selectButtonSelectors){try{if(s.includes(':contains(')){const kw=s.match(/:contains\("(.+?)"\)/)?.[1];if(!kw)continue;const b=s.replace(/:contains\(".+?"\)/,'');const l=b?card.querySelectorAll(b):card.querySelectorAll('button,a,[role="button"]');for(const e of l){if((e.textContent||'').includes(kw))return e}}else{const btn=card.querySelector(s);if(btn)return btn}}catch(e){}}for(const e of card.querySelectorAll('button,a,[onclick],[role="button"]')){const t=(e.textContent||'');if(t.includes('选择')||t.includes('选课'))return e}return null}
function visible(el){if(!el)return false;const r=el.getBoundingClientRect();if(r.width===0||r.height===0)return false;const s=getComputedStyle(el);return s.display!=='none'&&s.visibility!=='hidden'&&s.opacity!=='0'}
function clickConfirm(){for(const s of CONFIG.confirmButtonSelectors){try{if(s.includes(':contains(')){const kw=s.match(/:contains\("(.+?)"\)/)?.[1];if(!kw)continue;const b=s.replace(/:contains\(".+?"\)/,'');const l=b?document.querySelectorAll(b):document.querySelectorAll('button,a');for(const e of l){if((e.textContent||'').trim().includes(kw)&&visible(e)){e.click();return true}}}else{const btn=document.querySelector(s);if(btn&&visible(btn)){btn.click();return true}}}catch(e){}}for(const m of document.querySelectorAll('.modal,.dialog,.layer,[class*="modal" i],[class*="dialog" i],[role="dialog"]')){if(!visible(m))continue;for(const b of m.querySelectorAll('button,a.btn,.btn')){if(!visible(b))continue;const t=(b.textContent||'').trim();if(['确定','提交','确认','是'].some(k=>t===k||t.includes(k))){b.click();return true}}}return false}
let _oc=null;function hijackConfirm(){if(_oc===null){_oc=window.confirm;window.confirm=()=>true}}function restoreConfirm(){if(_oc!==null){window.confirm=_oc;_oc=null}}
function domScan(){
STATE.attemptCount++;
const target=getCurrentTarget();
if(!target){log('SUCCESS','✅ 所有课程抢课完成!');stopGrabber();return}
const cards=findCards();
if(!cards.length){if(STATE.attemptCount%5===0)log('WARN','未找到课程卡片');return}
let tc=null;
for(const c of cards){const cd=extractCode(c),nm=extractName(c);if((target.code&&cd===target.code)||(target.name&&nm&&nm.includes(target.name))){tc=c;if(!STATE.matchedCardFound){STATE.matchedCardFound=true;log('SUCCESS',`找到: [${cd||nm}] (进度 ${STATE.grabbedCount+1}/${STATE.maxGrab})`)}break}}
if(!tc){STATE.matchedCardFound=false;clickConfirm();return}
STATE.matchedCardFound=true;
const cap=parseCapacity(tc);if(!cap)return;
log('DATA',`[${target.code||target.name}] ${cap.sel}/${cap.cap} (剩余:${cap.cap-cap.sel})`);
if(cap.seltc.classList.remove('gb-highlight'),2000);
const btn=findSelectBtn(tc);if(!btn){log('ERROR','未找到选择按钮');return}
btn.click();log('INFO','已点击选择按钮');
const st=Date.now();const iv=setInterval(()=>{if(clickConfirm())clearInterval(iv);if(Date.now()-st>3000)clearInterval(iv)},150);
onGrabSuccess(target,'DOM点击');
}
clickConfirm();
}
// ==================== 主循环 ====================
function scanLoop(){if(!STATE.isRunning)return;if(STATE.apiMode){apiGrab().then(()=>scheduleNext())}else{domScan();scheduleNext()}}
function scheduleNext(){
if(!STATE.isRunning)return;let ms;
if(STATE.apiMode){if(STATE.rttHistory.length>0){const a=STATE.rttHistory.reduce((x,y)=>x+y,0)/STATE.rttHistory.length;ms=Math.max(CONFIG.apiSafetyFloor,a+CONFIG.apiMargin);ms+=(Math.random()-.5)*4}else{ms=200}}
else{ms=CONFIG.domPollMin+Math.random()*(CONFIG.domPollMax-CONFIG.domPollMin)}
STATE.timerId=setTimeout(scanLoop,ms);
}
function startGrabber(){
if(STATE.isRunning)return;
// 读取所有课程输入
const countEl=document.getElementById('gb-count');
const count=countEl?parseInt(countEl.value):1;
STATE.maxGrab=count;
STATE.targetList=[];
for(let i=0;i0){STATE.apiTemplate=STATE.capturedAPIs[0];STATE.apiMode=true}
STATE.isRunning=true;STATE.attemptCount=0;STATE.grabbedCount=0;STATE.currentIdx=0;
STATE.matchedCardFound=false;STATE.logLines=[];
const box=document.getElementById('gb-log');if(box)box.innerHTML='';
const names=STATE.targetList.map(t=>`[${t.code||t.name}]`).join(', ');
const mode=STATE.apiMode?'⚡API直连':'🐢DOM模式';
log('STATUS',`抢课计划: ${STATE.maxGrab}门 → ${names}`);
log('PROGRESS',`━━━ 开始监控第 1 门: [${STATE.targetList[0].code||STATE.targetList[0].name}] ━━━`);
log('INFO',`模式: ${mode} | 自适应间隔 = 平均RTT+3ms,最低${CONFIG.apiSafetyFloor}ms`);
hijackConfirm();
if('Notification' in window&&Notification.permission==='default')Notification.requestPermission();
scanLoop();updateBtns();
}
function stopGrabber(){
STATE.isRunning=false;
if(STATE.timerId){clearTimeout(STATE.timerId);STATE.timerId=null}
restoreConfirm();
const done=STATE.targetList.filter(t=>t.status==='grabbed').length;
log('STATUS',`监控已停止。抢到 ${done}/${STATE.targetList.length} 门`);
updateBtns();
}
function updateBtns(){
const s=document.getElementById('gb-start'),t=document.getElementById('gb-stop'),c=document.getElementById('gb-count');
if(s)s.disabled=STATE.isRunning;
if(t)t.disabled=!STATE.isRunning;
if(c)c.disabled=STATE.isRunning;
for(let i=0;i<6;i++){const inp=document.getElementById('gb-code-'+i);if(inp)inp.disabled=STATE.isRunning}
}
// ==================== API面板 ====================
function updateAPIPanel(){
const p=document.getElementById('gb-api-list'),s=document.getElementById('gb-api-status');
if(!p||!s)return;
if(STATE.capturedAPIs.length===0){p.innerHTML='等待捕获接口...
请手动操作一次选课流程
';s.textContent='🔍 监听中...';s.style.color='#fa0'}
else{p.innerHTML=STATE.capturedAPIs.map((a,i)=>{const ac=STATE.apiTemplate&&STATE.apiTemplate.url===a.url;return`${a.method} ${a.url.substring(0,60)}...
`}).join('');s.textContent=`✅ 已捕获 ${STATE.capturedAPIs.length} 个接口`;s.style.color='#0f0';p.querySelectorAll('.gb-api-item').forEach(e=>e.addEventListener('click',()=>{const i=+e.dataset.idx;STATE.apiTemplate=STATE.capturedAPIs[i];STATE.apiMode=true;updateAPIPanel();updateModeIndicator();log('API','已选择API: '+STATE.apiTemplate.method+' '+STATE.apiTemplate.url.substring(0,60))}))}
}
function updateModeIndicator(){const e=document.getElementById('gb-mode-indicator');if(e){e.textContent=STATE.apiMode?'⚡ API直连模式':'🐢 DOM模式';e.style.color=STATE.apiMode?'#0af':'#888'}}
// ==================== 输入框生成 ====================
function generateInputs(count){
const container=document.getElementById('gb-inputs');
if(!container)return;
let html='';
for(let i=0;i`}
container.innerHTML=html;
// 给每个输入框绑定回车键不触发起始
for(let i=0;i{if(e.key==='Enter'){e.preventDefault();const next=document.getElementById('gb-code-'+(i+1));if(next)next.focus();else startGrabber()}})}
}
// ==================== UI ====================
const US=document.createElement('style');
US.textContent=`@keyframes gbFadeIn{from{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}@keyframes gbPulse{0%,100%{box-shadow:0 0 5px #00ff88}50%{box-shadow:0 0 20px #00ff88,0 0 40px #00ff88}}.gb-highlight{animation:gbPulse .6s ease-in-out 3!important;outline:3px solid #00ff88!important;border-radius:8px!important}#gb-ui *{box-sizing:border-box}`;
document.head.appendChild(US);
function createUI(){
const ui=document.createElement('div');ui.id='gb-ui';
ui.style.cssText='position:fixed;top:20px;right:20px;width:370px;max-height:85vh;z-index:99998;background:linear-gradient(135deg,#0a0a1a,#1a1a2e);border:1px solid #00ff88;border-radius:12px;font-family:Courier New,Microsoft YaHei,monospace;color:#00ff88;display:flex;flex-direction:column;user-select:none;box-shadow:0 0 20px rgba(0,255,136,.15),0 8px 32px rgba(0,0,0,.5);';
ui.innerHTML=`
🎓 抢课工具 v3.1 ⚡ API直连
−
按顺序依次抢
📡 API嗅探器🔍 监听中...
💡 手动点一次选课,脚本自动学习API
就绪 - 设置抢课数量并填入代码
⚡ 仅限个人学习自用 | 请勿用于违规操作
`;
document.body.appendChild(ui);
document.getElementById('gb-start').addEventListener('click',startGrabber);
document.getElementById('gb-stop').addEventListener('click',stopGrabber);
document.getElementById('gb-count').addEventListener('change',function(){generateInputs(parseInt(this.value))});
let min=false;
document.getElementById('gb-min').addEventListener('click',()=>{min=!min;document.getElementById('gb-body').style.display=min?'none':'';document.getElementById('gb-min').textContent=min?'+':'−'});
// 拖拽
const dr=document.getElementById('gb-drag');let ox=0,oy=0,dg=false;
dr.addEventListener('mousedown',e=>{if(e.target.id==='gb-min')return;dg=true;const r=ui.getBoundingClientRect();ox=e.clientX-r.left;oy=e.clientY-r.top;dr.style.cursor='grabbing';document.body.style.userSelect='none'});
document.addEventListener('mousemove',e=>{if(!dg)return;let x=e.clientX-ox,y=e.clientY-oy;const mx=innerWidth-ui.offsetWidth,my=innerHeight-ui.offsetHeight;if(x<0)x=0;if(y<0)y=0;if(x>mx)x=mx;if(y>my)y=my;ui.style.left=x+'px';ui.style.top=y+'px';ui.style.right='auto'});
document.addEventListener('mouseup',()=>{dg=false;dr.style.cursor='grab';document.body.style.userSelect=''});
generateInputs(3); // 默认3门
updateModeIndicator();
}
// ==================== 初始化 ====================
function init(){
(function dp(){if(document.head)injectPreconnect();else setTimeout(dp,10)})();
setupAPISniffer();
if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',()=>setTimeout(createUI,300))}else{setTimeout(createUI,300)}
if('Notification' in window&&Notification.permission==='default')Notification.requestPermission();
console.log('[抢课工具 v3.1] 已加载 - 多课程顺序抢课模式');
}
init();
})();