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