// ==UserScript== // @name 超星MOOC人脸信息采集跳过(updateqrstatus版) // @namespace http://tampermonkey.net/ // @version 3.0.0 // @description 新版「人脸信息采集」跳过:读隐藏表单参数 -> POST updateqrstatus 标记已采集,并摘除弹窗/强制播放 // @author 焱炆 // @match https://mooc1.chaoxing.com/* // @match https://mooc1-2.chaoxing.com/* // @match https://mooc2-1.chaoxing.com/* // @match https://mooc1-1.chaoxing.com/* // @run-at document-start // @grant unsafeWindow // @grant GM_xmlhttpRequest // @connect mooc1-api.chaoxing.com // @connect pan-yz.chaoxing.com // @connect passport2-api.chaoxing.com // ==/UserScript== (function () { 'use strict'; const log = (m) => console.log('[采集跳过] ' + m); const warn = (m) => console.warn('[采集跳过] ' + m); const win = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; // 从主页面隐藏 input 读参数 function val(id) { const el = document.getElementById(id); return el ? (el.value || '').trim() : ''; } // 新版视频采集的真实参数名(带 Qr 后缀 / video 前缀) function getQRParams() { return { uuid: val('videouuid'), qrcEnc: val('videoqrcEnc'), courseId: val('fccourseIdQr'), clazzId: val('fcclazzIdQr'), chapterId: val('fcchapterIdQr'), cpi: val('fccpiQr') }; } // 生成随机 objectId(超星只要求非空,服务端不校验真实性) function randOid() { const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; let s = ''; for (let i = 0; i < 32; i++) s += chars[Math.floor(Math.random() * chars.length)]; return s; } let submitted = new Set(); // 已提交过的 uuid,避免重复打接口 // 核心:POST updateqrstatus,把当前 uuid 标记为已采集 function submitPass() { const p = getQRParams(); const uuid = p.uuid, clazzId = p.clazzId, courseId = p.courseId, qrcEnc = p.qrcEnc; if (!uuid || !clazzId || !courseId) { return false; } if (submitted.has(uuid)) { return true; } submitted.add(uuid); const oid = randOid(); const failCount = (() => { const e = document.querySelector('.faceVideoCheckFailCount'); return e ? (e.textContent || '').trim() : '0'; })(); const data = 'clazzId=' + encodeURIComponent(clazzId) + '&courseId=' + encodeURIComponent(courseId) + '&uuid=' + encodeURIComponent(uuid) + '&qrcEnc=' + encodeURIComponent(qrcEnc) + '&objectId=' + encodeURIComponent(oid) + '&failCount=' + encodeURIComponent(failCount) + '&compareResult=0'; log('提交 updateqrstatus: uuid=' + uuid + ' clazzId=' + clazzId + ' courseId=' + courseId); GM_xmlhttpRequest({ method: 'POST', url: 'https://mooc1-api.chaoxing.com/qr/updateqrstatus', data: data, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, onload: function (res) { log('updateqrstatus 响应: ' + res.status + ' ' + (res.responseText || '').slice(0, 300)); }, onerror: function (err) { warn('updateqrstatus 请求失败: ' + JSON.stringify(err)); submitted.delete(uuid); // 失败则允许重试 } }); return true; } // 摘除弹窗 function removeMasks() { const selectors = [ '.chapterVideoFaceQrMaskDiv', '.faceCollectQrPop', '.faceCollectQrPopVideo', '.chapterVideoFaceMaskDiv', '.maskDiv1' ]; selectors.forEach((sel) => { document.querySelectorAll(sel).forEach((el) => { if (el.style.display === 'none' || el.offsetParent === null) return; log('移除弹窗: ' + sel); el.style.display = 'none'; el.style.visibility = 'hidden'; if (el.parentNode) { try { el.parentNode.removeChild(el); } catch (e) {} } }); }); } // 强制播放器继续:找到视频元素强行 play function forcePlay() { try { const videos = document.querySelectorAll('video'); videos.forEach((v) => { if (v.paused) { const p = v.play(); if (p && p.catch) p.catch(() => {}); } }); if (videos.length) log('已强制播放 ' + videos.length + ' 个视频'); } catch (e) {} // 超星常用:关闭遮罩后调用自身的 play try { if (win.player && typeof win.player.play === 'function') { win.player.play(); } } catch (e) {} } let dumped = false; function dumpInputs() { if (dumped) return; dumped = true; const p = getQRParams(); log('新版参数: uuid=' + p.uuid + ' qrcEnc=' + p.qrcEnc + ' courseId=' + p.courseId + ' clazzId=' + p.clazzId + ' chapterId=' + p.chapterId + ' cpi=' + p.cpi); log('fcqrimg存在=' + !!(document.getElementById('fcqrimg'))); log('hasQR容器=' + !!(document.querySelector('.faceCollectQrPop, .faceCollectQrPopVideo'))); log('videoCaptureGetQrStatuse=' + val('videoCaptureGetQrStatuse')); } function tick() { // 判断是否出现采集弹窗(任一特征命中) const hasQR = !!(document.getElementById('fcqrimg')); const hasMask = document.querySelector('.chapterVideoFaceQrMaskDiv, .faceCollectQrPop, .faceCollectQrPopVideo'); if (hasQR || hasMask) { log('检测到人脸采集弹窗'); dumpInputs(); removeMasks(); forcePlay(); } } // 拦截 getqrstatus 的 fetch/XHR:直接伪造「已采集成功」响应 // 真实成功结构(抓包所得):{"code":"0","failCount":"0","videoFaceCaptureSuccessEnc":"...","compareResult":"0","status":2} let transportHooked = false; const FAKE_SUCCESS = JSON.stringify({ code: '0', failCount: '0', videoFaceCaptureSuccessEnc: '024800adaf452a7daa71a9c57bb1eb14', compareResult: '0', status: 2 }); function observeGetQrStatus() { if (transportHooked) return; transportHooked = true; // fetch const origFetch = win.fetch; if (origFetch && !win.__qrFetchHooked) { win.__qrFetchHooked = true; win.fetch = function (url, opts) { const u = (typeof url === 'string') ? url : (url && url.url) || ''; if (u.indexOf('getqrstatus') !== -1) { log('伪造 getqrstatus 响应 -> status=2 (已采集)'); return Promise.resolve(new Response(FAKE_SUCCESS, { status: 200, headers: { 'Content-Type': 'application/json' } })); } return origFetch.apply(win, arguments); }; } // XMLHttpRequest const origXHR = win.XMLHttpRequest; if (origXHR && !win.__qrXHRHooked) { win.__qrXHRHooked = true; win.XMLHttpRequest = function () { const xhr = new origXHR(); const origOpen = xhr.open; xhr.open = function (method, url) { xhr.__isQR = (url && url.indexOf('getqrstatus') !== -1); return origOpen.apply(xhr, arguments); }; const origSend = xhr.send; xhr.send = function () { if (xhr.__isQR) { log('伪造 getqrstatus 响应 -> status=2 (已采集)'); Object.defineProperty(xhr, 'readyState', { value: 4 }); Object.defineProperty(xhr, 'status', { value: 200 }); Object.defineProperty(xhr, 'responseText', { value: FAKE_SUCCESS }); Object.defineProperty(xhr, 'response', { value: FAKE_SUCCESS }); try { if (xhr.onreadystatechange) xhr.onreadystatechange(); if (xhr.onload) xhr.onload(); } catch (e) {} return; } return origSend.apply(xhr, arguments); }; return xhr; }; try { win.XMLHttpRequest.prototype = origXHR.prototype; } catch (e) {} } log('已接管 getqrstatus,将始终返回已采集(status=2)'); } function start() { log('启动完成,等待采集弹窗'); // 提前接管 getqrstatus,轮询一启动就返回已采集 observeGetQrStatus(); setInterval(tick, 1500); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } })();