// ==UserScript==
// @name 青书学堂API刷课助手 By:王杰
// @namespace https://github.com/barryallennnnn
// @version 1.0.2
// @description 青书学堂API刷课脚本,自动检测学校API地址
// @author lidppp
// @match *://*.qingshuxuetang.com/*
// @match *://*.qingshuxuetang.com.cn/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @connect *
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const MAX_RETRY = 3;
const MAX_POSITION = 1000;
const REPORT_INTERVAL = 120000;
GM_addStyle(`
#qs-panel{position:fixed;top:20px;right:20px;width:380px;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.3);z-index:99999;font-family:system-ui,sans-serif;color:#fff;overflow:hidden}
.qs-hd{background:rgba(0,0,0,.2);padding:10px 14px;display:flex;justify-content:space-between;align-items:center;cursor:move}
.qs-hd h3{margin:0;font-size:14px}
.qs-hd button{background:none;border:none;color:#fff;font-size:16px;cursor:pointer}
.qs-bd{padding:14px;max-height:500px;overflow-y:auto}
.qs-tip{background:rgba(255,193,7,.2);border-left:3px solid #ffc107;padding:8px 10px;border-radius:0 6px 6px 0;margin-bottom:12px;font-size:11px;line-height:1.5}
.qs-gp{margin-bottom:10px}
.qs-gp label{display:block;margin-bottom:4px;font-size:12px;opacity:.9}
.qs-gp input,.qs-gp select{width:100%;padding:8px 10px;border:none;border-radius:6px;background:rgba(255,255,255,.9);color:#000;font-size:13px;box-sizing:border-box}
.qs-gp input:focus,.qs-gp select:focus{outline:none;background:rgba(255,255,255,.25)}
.qs-gp select option{background:#333;color:#fff}
.qs-row{display:flex;gap:8px}
.qs-btn{flex:1;padding:8px;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer}
.qs-btn-ok{background:#4caf50;color:#fff}
.qs-btn-no{background:#f44336;color:#fff}
.qs-btn-2{background:rgba(255,255,255,.2);color:#fff}
.qs-btn:disabled{opacity:.5;cursor:not-allowed}
.qs-stat{background:rgba(0,0,0,.15);border-radius:6px;padding:10px;margin:10px 0;font-size:12px}
.qs-task{background:rgba(0,0,0,.15);border-radius:6px;padding:8px 10px;margin:6px 0;font-size:11px}
.qs-task-ok{border-left:3px solid #4caf50}
.qs-task-err{border-left:3px solid #f44336}
.qs-task-run{border-left:3px solid #2196f3}
.qs-log{background:rgba(0,0,0,.2);border-radius:6px;padding:8px;margin-top:10px;max-height:120px;overflow-y:auto;font-family:monospace;font-size:10px;line-height:1.5}
.qs-log div{padding:1px 0}
.qs-t{opacity:.5;margin-right:6px}
.qs-i{color:#81d4fa}
.qs-ok{color:#81c784}
.qs-err{color:#e57373}
.qs-w{color:#ffd54f}
`);
function log(msg, type) {
type = type || 'i';
var el = document.getElementById('qs-log');
if (el) {
var t = new Date().toLocaleTimeString();
el.innerHTML += '
[' + t + ']' + msg + '
';
el.scrollTop = el.scrollHeight;
}
console.log('[青书刷课] ' + msg);
}
function getCookie(name) {
var m = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return m ? m[2] : '';
}
function httpGet(url, cb) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: { 'Accept': 'application/json' },
timeout: 15000,
onload: function(r) {
try { cb(null, JSON.parse(r.responseText)); }
catch(e) { cb(null, null); }
},
onerror: function(e) { cb(e || new Error('error')); },
ontimeout: function() { cb(new Error('timeout')); }
});
}
function httpPost(url, data, cb) {
var token = document.getElementById('qs-token') ? document.getElementById('qs-token').value.trim() : '';
if (!token) token = getCookie('AccessToken') || '';
var opts = {
method: 'POST',
url: url,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': 'AccessToken=' + token,
'Referer': url,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
},
timeout: 30000,
onload: function(r) {
log('HTTP状态: ' + r.status, r.status === 200 ? 'ok' : 'err');
if (r.status === 200) {
try {
var json = JSON.parse(r.responseText);
cb(null, json);
}
catch(e) {
log('JSON解析失败', 'err');
cb(null, { hr: -1, message: '解析失败' });
}
} else {
log('HTTP错误: ' + r.status, 'err');
cb(null, { hr: -1, message: 'HTTP ' + r.status });
}
},
onerror: function(e) {
log('网络错误', 'err');
cb(e || new Error('error'));
},
ontimeout: function() {
log('请求超时', 'err');
cb(new Error('timeout'));
}
};
if (data instanceof FormData) {
opts.data = data;
delete opts.headers['Content-Type'];
} else {
opts.data = JSON.stringify(data);
}
GM_xmlhttpRequest(opts);
}
// 自动检测学校API - 从当前页面URL推断
function detectSchoolApi(cb) {
var path = window.location.pathname;
log('当前路径: ' + path, 'i');
// 从路径中提取学校代码,例如 /tykj/Student/Course/CourseStudy -> /tykj/Student
var match = path.match(/^(\/[^\/]+\/[^\/]+)\//);
if (match && match[1]) {
var apiPath = match[1];
log('API路径: ' + apiPath, 'ok');
// 构造API地址
var apiBase = location.origin + apiPath;
log('API地址: ' + apiBase, 'ok');
document.getElementById('qs-api').value = apiBase;
cb(apiBase);
} else {
log('无法从路径推断API地址', 'w');
log('使用当前域名: ' + location.origin, 'i');
document.getElementById('qs-api').value = location.origin;
cb(location.origin);
}
}
// 测试API路径
function testApiPath(base, path, cb) {
var url = base + path;
log('测试: ' + url, 'i');
// 使用实际参数测试
var testParams = {
contentId: 'test',
contentType: 11,
courseId: '959',
teachPlanId: '558',
periodId: '25',
classId: '558'
};
httpPost(url, testParams, function(err, data) {
if (err) {
log('失败: ' + (err.message || '网络错误'), 'err');
cb(false);
} else {
log('响应: ' + JSON.stringify(data), 'i');
cb(true, data);
}
});
}
// 测试多个API路径
function findWorkingApiPath(base, cb) {
var paths = [
'/Course/UploadStudyRecordBegin',
'/Student/Course/UploadStudyRecordBegin',
'/Course/CourseData',
'/Student/Course/CourseData',
];
var index = 0;
function testNext() {
if (index >= paths.length) {
log('所有路径都失败', 'err');
cb(null);
return;
}
var path = paths[index];
index++;
testApiPath(base, path, function(success, data) {
if (success && data && data.hr === 0) {
log('找到可用API: ' + base + path, 'ok');
cb(base + path);
} else {
testNext();
}
});
}
testNext();
}
// 检测课程
function detectCourse() {
if (!window.location.pathname.includes('CourseStudy')) return null;
var u = new URL(window.location.href);
var params = {
courseId: u.searchParams.get('courseId') || '',
teachPlanId: u.searchParams.get('teachPlanId') || '',
periodId: u.searchParams.get('periodId') || '',
classId: u.searchParams.get('classId') || ''
};
if (!params.courseId) return null;
var nodes = [];
var links = document.querySelectorAll('.left_part .level-1 a[id]');
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute('href');
var m = href ? href.match(/.*\('([^']+)'.*/) : null;
if (m && m[1]) {
// 检查是否已学习(有mark标记表示已学习)
var isLearned = !!links[i].querySelector('span.mark');
nodes.push({
contentId: m[1],
contentType: 11,
courseId: params.courseId,
teachPlanId: params.teachPlanId,
periodId: params.periodId,
classId: params.teachPlanId,
learned: isLearned
});
}
}
// 统计未学习的课件
var unlearned = nodes.filter(function(n) { return !n.learned; });
log('总课件: ' + nodes.length + ', 未学习: ' + unlearned.length, 'i');
return { params: params, nodes: unlearned, title: document.title };
}
// 任务类
function Task(base, params, onUpdate) {
this.base = base;
this.params = params;
this.onUpdate = onUpdate;
this.recordId = -1;
this.pos = 0;
this.retry = 0;
this.timer = null;
this.done = false;
}
Task.prototype.begin = function() {
if (this.done) return;
this.retry++;
if (this.retry > MAX_RETRY) {
this.onUpdate('fail', '重试次数用尽');
return;
}
var self = this;
var url = this.base + '/Course/UploadStudyRecordBegin';
log('POST ' + url, 'i');
log('参数: ' + JSON.stringify(this.params), 'i');
this.onUpdate('begin', '第' + this.retry + '次获取...');
httpPost(url, this.params, function(err, data) {
if (self.done) return;
if (err) {
log('请求失败: ' + (err.message || ''), 'err');
setTimeout(function() { self.begin(); }, 5000);
return;
}
log('响应: ' + JSON.stringify(data), 'i');
if (data.hr === 0) {
self.recordId = data.data;
log('recordId=' + self.recordId, 'ok');
self.onUpdate('ready');
// 随机延迟60-120秒后开始上报
var delay = Math.floor(Math.random() * 60000) + 60000;
log('等待 ' + Math.round(delay/1000) + ' 秒后开始上报', 'i');
setTimeout(function() { self.run(); }, delay);
} else {
log('失败: hr=' + data.hr + ' msg=' + (data.message || ''), 'err');
setTimeout(function() { self.begin(); }, 5000);
}
});
};
Task.prototype.run = function() {
if (this.done) return;
this.onUpdate('run');
this.report();
};
Task.prototype.report = function() {
if (this.done) return;
var self = this;
this.pos += Math.floor(Math.random() * 41) + 60;
if (this.pos >= MAX_POSITION) {
log('进度完成', 'ok');
this.end();
return;
}
var url = this.base + '/Course/UploadStudyRecordContinue?_t=' + Date.now();
log('上报: recordId=' + this.recordId + ' pos=' + this.pos, 'i');
// 使用FormData格式,与原始代码一致
var fd = new FormData();
fd.append('recordId', this.recordId);
fd.append('end', 'false');
fd.append('position', this.pos);
fd.append('timeOutConfirm', 'false');
httpPost(url, fd, function(err, resp) {
if (self.done) return;
if (err) {
log('上报错误: ' + (err.message || ''), 'err');
} else {
log('上报响应: ' + JSON.stringify(resp), 'i');
if (resp.hr === 0) {
log('进度 ' + self.pos + '/' + MAX_POSITION, 'ok');
} else {
log('上报失败: ' + (resp.message || 'hr=' + resp.hr), 'err');
}
}
self.onUpdate('run', self.pos);
if (!self.done) {
// 等待2分钟后再次上报
log('等待 ' + (REPORT_INTERVAL/1000) + ' 秒后再次上报', 'i');
self.timer = setTimeout(function() { self.report(); }, REPORT_INTERVAL);
}
});
};
Task.prototype.end = function() {
var self = this;
var url = this.base + '/Course/UploadStudyRecordContinue?_t=' + Date.now();
// 使用FormData格式
var fd = new FormData();
fd.append('recordId', this.recordId);
fd.append('end', 'true');
fd.append('position', this.pos);
fd.append('timeOutConfirm', 'false');
httpPost(url, fd, function(err, resp) {
self.done = true;
if (err) {
log('结束请求失败', 'err');
} else {
log('结束响应: ' + JSON.stringify(resp), 'i');
}
self.onUpdate('done');
log('任务完成!', 'ok');
});
};
Task.prototype.stop = function() {
this.done = true;
clearTimeout(this.timer);
};
// 主应用
function App() {
this.course = null;
this.tasks = [];
this.running = false;
this.current = 0;
this.apiBase = '';
this.init();
}
App.prototype.init = function() {
this.createUI();
this.autoDetect();
// 延迟检测课程,等待页面DOM加载完成
var self = this;
setTimeout(function() { self.detect(); }, 1000);
// 再次延迟检测,确保动态内容加载完成
setTimeout(function() { self.detect(); }, 3000);
};
App.prototype.createUI = function() {
var div = document.createElement('div');
div.id = 'qs-panel';
div.innerHTML = '' +
'
青书学堂刷课助手 By:王杰
' +
'' +
'' +
'' +
'
自动检测学校API,进入课程页面后点击"开始刷课"。
' +
'
' +
'' +
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
未检测到课程
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'' +
'
' +
'
' +
'
' +
'
';
document.body.appendChild(div);
// 拖拽
var hd = div.querySelector('.qs-hd');
var drag = false, sx, sy, sl, st;
hd.onmousedown = function(e) {
drag = true; sx = e.clientX; sy = e.clientY;
var r = div.getBoundingClientRect(); sl = r.left; st = r.top;
div.style.position = 'fixed'; div.style.left = sl + 'px'; div.style.top = st + 'px';
};
document.onmousemove = function(e) { if (drag) { div.style.left = (sl + e.clientX - sx) + 'px'; div.style.top = (st + e.clientY - sy) + 'px'; } };
document.onmouseup = function() { drag = false; };
var self = this;
document.getElementById('qs-min').onclick = function() { div.style.display = 'none'; };
document.getElementById('qs-start').onclick = function() { self.start(); };
document.getElementById('qs-stop').onclick = function() { self.stop(); };
document.getElementById('qs-refresh').onclick = function() { self.detect(); self.autoDetect(); };
document.getElementById('qs-gettk').onclick = function() { self.getToken(); };
document.getElementById('qs-save').onclick = function() { self.save(); };
document.getElementById('qs-test').onclick = function() { self.testApi(); };
// 加载配置
var savedApi = GM_getValue('qs-api', '');
if (savedApi && savedApi.startsWith('http')) {
document.getElementById('qs-api').value = savedApi;
}
document.getElementById('qs-token').value = GM_getValue('qs-token', '');
log('脚本已加载', 'ok');
};
App.prototype.autoDetect = function() {
var self = this;
detectSchoolApi(function(url) {
self.apiBase = url;
document.getElementById('qs-api').value = url;
GM_setValue('qs-api', url);
});
};
App.prototype.detect = function() {
this.course = detectCourse();
var stat = document.getElementById('qs-stat');
var btn = document.getElementById('qs-start');
if (this.course) {
var total = this.course.nodes.length;
stat.innerHTML = '✓ ' + this.course.title + '
待学习课件: ' + total + ' 个';
btn.disabled = total === 0;
if (total > 0) {
log('检测到 ' + total + ' 个待学习课件', 'ok');
} else {
log('所有课件已学习完成!', 'ok');
}
} else {
stat.innerHTML = '✗ 未检测到课程页面';
btn.disabled = true;
}
};
App.prototype.getToken = function() {
var token = getCookie('AccessToken');
if (token) {
document.getElementById('qs-token').value = token;
log('已获取Token', 'ok');
} else {
log('Cookie中无Token', 'w');
log('F12执行: document.cookie', 'i');
}
};
App.prototype.save = function() {
var api = document.getElementById('qs-api').value.trim().replace(/\/+$/, '');
document.getElementById('qs-api').value = api;
GM_setValue('qs-api', api);
GM_setValue('qs-token', document.getElementById('qs-token').value.trim());
log('配置已保存', 'ok');
};
App.prototype.start = function() {
if (!this.course || !this.course.nodes || !this.course.nodes.length) return;
var base = document.getElementById('qs-api').value.trim().replace(/\/+$/, '');
if (!base) { log('请填写API地址', 'err'); return; }
this.save();
this.running = true;
this.current = 0;
this.tasks = [];
document.getElementById('qs-start').disabled = true;
document.getElementById('qs-stop').disabled = false;
document.getElementById('qs-tasks').innerHTML = '';
log('开始刷课,共 ' + this.course.nodes.length + ' 个任务', 'ok');
this.processNext(base);
};
App.prototype.processNext = function(base) {
if (!this.running || this.current >= this.course.nodes.length) {
if (this.running) {
log('所有任务完成!', 'ok');
this.running = false;
document.getElementById('qs-start').disabled = false;
document.getElementById('qs-stop').disabled = true;
// 所有任务完成后刷新页面
log('3秒后刷新页面...', 'i');
setTimeout(function() { location.reload(); }, 3000);
}
return;
}
var node = this.course.nodes[this.current];
var idx = this.current;
var self = this;
var div = document.createElement('div');
div.className = 'qs-task qs-task-run';
div.id = 'qs-t-' + idx;
div.textContent = '任务' + (idx + 1) + ': ' + node.contentId;
document.getElementById('qs-tasks').appendChild(div);
var task = new Task(base, node, function(status, pos) {
var el = document.getElementById('qs-t-' + idx);
if (!el) return;
if (status === 'done' || status === 'fail') {
el.className = 'qs-task ' + (status === 'done' ? 'qs-task-ok' : 'qs-task-err');
el.textContent = '任务' + (idx + 1) + ': ' + node.contentId + ' [' + (status === 'done' ? '完成' : '失败') + ']';
self.current++;
// 每个任务完成后刷新页面,然后继续下一个
log('任务完成,刷新页面...', 'i');
setTimeout(function() { location.reload(); }, 3000);
} else {
el.textContent = '任务' + (idx + 1) + ': ' + node.contentId + ' [' + status + (pos !== undefined ? ' ' + pos + '/1000' : '') + ']';
}
});
this.tasks.push(task);
task.begin();
};
App.prototype.stop = function() {
this.running = false;
this.tasks.forEach(function(t) { t.stop(); });
document.getElementById('qs-start').disabled = false;
document.getElementById('qs-stop').disabled = true;
log('已停止', 'w');
};
App.prototype.testApi = function() {
var base = document.getElementById('qs-api').value.trim().replace(/\/+$/, '');
if (!base) {
log('请填写API地址', 'err');
return;
}
log('开始测试API路径...', 'i');
findWorkingApiPath(base, function(url) {
if (url) {
log('找到可用API: ' + url, 'ok');
} else {
log('未找到可用API', 'err');
}
});
};
// 启动
if (document.readyState === 'complete') {
new App();
} else {
window.addEventListener('load', function() { new App(); });
}
})();