// ==UserScript==
// @name 超星学习通人脸强制通过(QQ交流群:164902340)
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @description 实现稳定的超星MOOC人脸识别,自动通过
// @author Doubao
// @match https://mooc1-2.chaoxing.com/mooc-ans/mycourse/studentstudy*
// @match https://mooc1-2.chaoxing.com/visit/stucoursemiddle*
// @match https://mooc1.chaoxing.com/mooc-ans/mycourse/studentstudy*
// @match https://mooc1.chaoxing.com/visit/stucoursemiddle*
// @match https://mooc2-1.chaoxing.com/mooc-ans/mycourse/studentstudy*
// @match https://mooc2-1.chaoxing.com/visit/stucoursemiddle*
// @match https://mooc1.chaoxing.com/mycourse/studentstudy*
// @grant GM_download
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @require https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.min.js
// ==/UserScript==
(function() {
'use strict';
// 全局状态管理
const globalState = {
isProcessing: false,
lastQRUrl: '',
lastProcessTime: 0,
initialized: false
};
// 日志系统
const logger = {
info: (msg) => console.log(`[超星人脸助手] ${msg}`),
error: (msg) => console.error(`[超星人脸助手] ${msg}`),
debug: (msg) => console.debug(`[超星人脸助手] ${msg}`),
warn: (msg) => console.warn(`[超星人脸助手] ${msg}`),
verbose: (msg) => console.log(`[超星人脸助手-详细] ${msg}`)
};
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Session管理
const session = {
cookies: {},
setCookie: (key, value) => {
session.cookies[key] = value;
},
getCookie: (key) => {
return session.cookies[key] || null;
},
get: (url, params, callback) => {
const queryString = params ? '?' + Object.keys(params).map(key =>
encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
).join('&') : '';
fetch(url + queryString, {
method: 'GET',
credentials: 'include'
}).then(response => {
if (response.ok) {
return response.arrayBuffer ? response.arrayBuffer() : response.text();
} else {
throw new Error(`HTTP ${response.status}`);
}
}).then(data => {
callback({
error: false,
status: 200,
responseText: typeof data === 'string' ? data : null,
response: typeof data !== 'string' ? data : null
});
}).catch(error => {
callback({
error: true,
status: 500,
message: error.message
});
});
},
post: (url, data, callback, headers = {}) => {
fetch(url, {
method: 'POST',
body: data,
credentials: 'include',
headers: headers
}).then(response => {
return response.text().then(text => ({
status: response.status,
responseText: text,
error: !response.ok
}));
}).then(result => {
callback(result);
}).catch(error => {
callback({
error: true,
status: 500,
message: error.message
});
});
}
};
// 通知系统
function showCustomNotification(title, message, type = 'info') {
const colors = {
success: '#4CAF50',
error: '#f44336',
warning: '#ff9800',
info: '#2196F3'
};
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${colors[type]};
color: white;
padding: 15px;
border-radius: 5px;
z-index: 10001;
max-width: 300px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
`;
notification.innerHTML = `${title}
${message}`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
}
// QR码参数提取器
class FaceQRExtractor {
constructor() {
this.qrParams = {};
this.hiddenFields = {};
}
// 提取QR码URL
extractQRUrl() {
const fcqrimg = document.getElementById('fcqrimg');
if (fcqrimg && fcqrimg.src) {
return fcqrimg.src;
}
return null;
}
// 解析URL参数
parseUrlParams(url) {
const params = {};
try {
const urlObj = new URL(url, window.location.origin);
urlObj.searchParams.forEach((value, key) => {
params[key] = value;
});
if (params.content) {
try {
const decodedContent = decodeURIComponent(params.content);
const contentUrl = new URL(decodedContent);
contentUrl.searchParams.forEach((value, key) => {
params[key] = value;
});
} catch (e) {
logger.warn('解析content参数失败: ' + e.message);
}
}
} catch (e) {
logger.error('解析URL失败: ' + e.message);
}
return params;
}
// 提取隐藏字段
extractHiddenFields() {
const hiddenInputs = document.querySelectorAll('input[type="hidden"]');
const fields = {};
hiddenInputs.forEach(input => {
if (input.id && input.value) {
fields[input.id] = input.value;
}
});
return fields;
}
// 获取所有参数
getAllParams() {
const qrUrl = this.extractQRUrl();
if (!qrUrl) {
return null;
}
// 检查是否与上次相同,避免重复处理
if (qrUrl === globalState.lastQRUrl) {
return null;
}
globalState.lastQRUrl = qrUrl;
this.qrParams = this.parseUrlParams(qrUrl);
this.hiddenFields = this.extractHiddenFields();
// 从当前页面URL获取参数
const currentUrlParams = new URLSearchParams(window.location.search);
const allParams = {
...this.qrParams,
...this.hiddenFields,
// 优先使用QR码和隐藏字段的参数,如果没有则从当前URL获取
courseid: this.hiddenFields.curCourseId ||
this.qrParams.courseid ||
currentUrlParams.get('courseid'),
clazzid: this.hiddenFields.curClazzId ||
this.qrParams.clazzid ||
currentUrlParams.get('clazzid'),
cpi: this.hiddenFields.curCpi ||
this.qrParams.cpi ||
currentUrlParams.get('cpi'),
knowledgeid: this.hiddenFields.curChapterId ||
this.qrParams.knowledgeid ||
currentUrlParams.get('knowledgeid')
};
return allParams;
}
}
// 初始化Cookie
function initCookies() {
const cookies = document.cookie.split(';');
cookies.forEach(cookie => {
const [key, value] = cookie.trim().split('=');
if (key && value) {
session.setCookie(key, value);
}
});
}
// 添加签名
function addInfEncSign(params) {
if (!params) return {};
const query = Object.keys(params)
.filter(key => params[key] !== undefined && params[key] !== null)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join("&") + "&DESKey=Z(AfY@XS";
return {
...params,
"inf_enc": CryptoJS.MD5(query).toString()
};
}
// 获取时间戳函数
function getTs() {
return Date.now();
}
// 处理图片(整合两款脚本的图片处理逻辑,增强扰动)
function processImageDataPromise(imgBytes) {
return new Promise((resolve, reject) => {
if (!imgBytes || imgBytes.byteLength === 0) {
logger.error("图片数据为空,无法处理");
reject(new Error("图片数据为空"));
return;
}
try {
const uint8Array = new Uint8Array(imgBytes);
const img = new Image();
const blob = new Blob([uint8Array], { type: 'image/jpeg' });
const imgUrl = URL.createObjectURL(blob);
img.onload = () => {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error("无法获取Canvas上下文");
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imgData.data;
// 增强扰动策略:整合两款脚本的扰动逻辑并增强
for (let i = 0; i < Math.min(15, pixels.length / 100); i++) {
const y = Math.floor(Math.random() * img.height);
const x = Math.floor(Math.random() * img.width) * 4;
const c = Math.floor(Math.random() * 3);
const val = Math.floor(Math.random() * 5) - 2;
pixels[x + c] = Math.max(0, Math.min(255, pixels[x + c] + val));
}
ctx.putImageData(imgData, 0, 0);
canvas.toBlob((blob) => {
if (!blob) throw new Error("无法生成图片Blob");
const reader = new FileReader();
reader.onload = (e) => {
if (e.target.result) {
const processedBytes = new Uint8Array(e.target.result);
resolve(processedBytes);
} else {
throw new Error("无法读取图片数据");
}
};
reader.onerror = () => {
logger.error("读取处理后的图片数据失败");
reject(new Error("读取处理后的图片数据失败"));
};
reader.readAsArrayBuffer(blob);
}, 'image/jpeg', 0.8);
} catch (e) {
logger.error(`图片处理失败: ${e.message}`);
reject(e);
} finally {
URL.revokeObjectURL(imgUrl);
}
};
img.onerror = () => {
logger.error("图片加载失败");
reject(new Error("图片加载失败"));
};
img.src = imgUrl;
} catch (e) {
logger.error(`处理图片数据失败: ${e.message}`);
reject(e);
}
});
}
// 提交人脸识别函数
function submitFaceRecognition(params, objectId) {
return submitSign(params, objectId);
}
// 替换getUserTokenPromise函数(约第310行)
function getUserTokenPromise() {
return new Promise((resolve, reject) => {
const url = 'https://pan-yz.chaoxing.com/api/token/uservalid';
// 检查Cookie是否存在
const puid = session.getCookie('_uid');
if (!puid) {
logger.warn("未获取到用户ID,重新初始化Cookie");
initCookies();
}
// 使用GM_xmlhttpRequest来避免CORS问题
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': "Mozilla/5.0 (Linux; Android 12; SM-N9006 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36 (schild:a6aba57d51661f412e8b8d2177adf8b4) (device:SM-N9006) Language/zh_CN com.chaoxing.mobile/ChaoXingStudy_3_6.4.5_android_phone_10831_263 (@Kalimdor)_6e98c17c08204755905e1537b1933e72",
'Accept': 'application/json, text/plain, */*',
'Referer': window.location.href
},
onload: function(response) {
try {
if (response.status !== 200) {
throw new Error(`请求失败: ${response.status}`);
}
const data = JSON.parse(response.responseText);
if (data && data._token) {
resolve(data._token);
} else {
logger.error("获取token失败: " + (data?.message || "响应中缺少_token"));
reject(new Error("获取token失败"));
}
} catch (e) {
logger.error(`解析token响应失败: ${e.message}`);
reject(e);
}
},
onerror: function(error) {
logger.error(`Token请求失败: ${error}`);
reject(new Error("Token请求失败"));
}
});
});
}
// 替换getUserFaceImageDataPromise函数(约第350行)
function getUserFaceImageDataPromise(puid) {
return new Promise((resolve, reject) => {
const url = "https://passport2-api.chaoxing.com/api/getUserFaceid";
const params = {
"enc": CryptoJS.MD5(puid + "uWwjeEKsri").toString(),
"token": "4faa8662c59590c6f43ae9fe5b002b42",
"_time": getTs()
};
const signedParams = addInfEncSign(params);
const queryString = Object.keys(signedParams)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(signedParams[key])}`)
.join('&');
GM_xmlhttpRequest({
method: 'GET',
url: `${url}?${queryString}`,
headers: {
'User-Agent': "Mozilla/5.0 (Linux; Android 12; SM-N9006 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36 (schild:a6aba57d51661f412e8b8d2177adf8b4) (device:SM-N9006) Language/zh_CN com.chaoxing.mobile/ChaoXingStudy_3_6.4.5_android_phone_10831_263 (@Kalimdor)_6e98c17c08204755905e1537b1933e72",
'Accept': 'application/json, text/plain, */*',
'Referer': window.location.href
},
onload: function(response) {
try {
if (response.status !== 200) {
throw new Error(`请求失败: ${response.status}`);
}
const data = JSON.parse(response.responseText);
if (data && data.result === 1 && data.data && data.data.http) {
// 获取图片数据
GM_xmlhttpRequest({
method: 'GET',
url: data.data.http,
responseType: 'arraybuffer',
onload: function(imgResponse) {
if (imgResponse.status === 200 && imgResponse.response) {
logger.info("成功获取人脸图片二进制数据");
resolve(imgResponse.response);
} else {
logger.error(`获取人脸图片数据失败,状态码: ${imgResponse.status}`);
reject(new Error("获取人脸图片数据失败"));
}
},
onerror: function(error) {
logger.error(`获取人脸图片数据失败: ${error}`);
reject(new Error("获取人脸图片数据失败"));
}
});
} else {
logger.error(`获取人脸图片URL失败: ${data?.msg || "未知错误"}`);
reject(new Error("获取人脸图片URL失败"));
}
} catch (e) {
logger.error(`解析人脸图片响应失败: ${e.message}`);
reject(e);
}
},
onerror: function(error) {
logger.error(`人脸图片URL请求失败: ${error}`);
reject(new Error("人脸图片URL请求失败"));
}
});
});
}
// 替换uploadFaceImagePromise函数(约第450行)
function uploadFaceImagePromise(imgBytes, puid, uploadToken) {
return new Promise((resolve, reject) => {
const url = `https://pan-yz.chaoxing.com/upload?uploadtype=face&_token=${uploadToken}`;
const formData = new FormData();
try {
formData.append('file', new Blob([imgBytes], { type: 'image/jpeg' }), `${puid}.jpg`);
formData.append('puid', puid);
} catch (e) {
logger.error(`创建FormData失败: ${e.message}`);
reject(e);
return;
}
GM_xmlhttpRequest({
method: 'POST',
url: url,
data: formData,
headers: {
'User-Agent': "Mozilla/5.0 (Linux; Android 12; SM-N9006 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36 (schild:a6aba57d51661f412e8b8d2177adf8b4) (device:SM-N9006) Language/zh_CN com.chaoxing.mobile/ChaoXingStudy_3_6.4.5_android_phone_10831_263 (@Kalimdor)_6e98c17c08204755905e1537b1933e72",
'Accept': 'application/json, text/plain, */*',
'Referer': window.location.href
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
logger.verbose && logger.verbose("图片上传完整响应: " + response.responseText);
if (data && data.objectId) {
logger.info("图片上传成功,获取到objectId");
resolve(data.objectId);
} else {
logger.error(`上传失败: ${data?.message || "响应中没有objectId"}`);
reject(new Error(`上传失败: ${data?.message || "响应中没有objectId"}`));
}
} catch (e) {
logger.error(`解析上传响应失败: ${e.message}`);
reject(e);
}
} else {
logger.error(`图片上传失败,状态码: ${response.status}`);
reject(new Error(`图片上传失败,状态码: ${response.status}`));
}
},
onerror: function(error) {
logger.error(`图片上传请求失败: ${error}`);
reject(new Error("图片上传请求失败"));
}
});
});
}
// 替换submitSign函数(约第550行)
function submitSign(params, objectId, retryCount = 0) {
const maxRetries = 10;
const retryDelay = 5000;
const url = `https://mooc1-api.chaoxing.com/mooc-ans/qr/updateqrstatus?uuid2=${params.uuid}&clazzId2=${params.clazzid}`;
const formData = new FormData();
formData.append('clazzId', params.clazzid);
formData.append('courseId', params.courseid);
formData.append('uuid', params.uuid);
formData.append('qrcEnc', params.qrcEnc);
formData.append('cpi', params.cpi);
formData.append('liveDetectionStatus', "1");
formData.append('objectId', objectId);
formData.append('knowledgeid', params.knowledgeid || "0");
// 添加可能的空值参数
formData.append('signt', params.signt || "");
formData.append('signk', params.signk || "");
formData.append('cxtime', params.cxtime || "");
formData.append('cxcid', params.cxcid || "");
formData.append('videojobid', params.videojobid || "");
formData.append('videoCollectTime', params.realCollectTime || "0");
formData.append('chaptervideoobjectid', params.chaptervideoobjectid || "");
logger.info(`准备提交人脸请求 (重试次数: ${retryCount})`);
// 打印FormData的详细内容
const formDataEntries = [];
for (let [key, value] of formData.entries()) {
formDataEntries.push(`${key}: ${value}`);
}
// 也可以打印为JSON格式
const formDataObj = {};
for (let [key, value] of formData.entries()) {
formDataObj[key] = value;
}
logger.info(`准备提交人脸参数JSON: ${JSON.stringify(formDataObj, null, 2)}`);
GM_xmlhttpRequest({
method: 'POST',
url: url,
data: formData,
headers: {
'User-Agent': "Mozilla/5.0 (Linux; Android 12; SM-N9006 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36 (schild:a6aba57d51661f412e8b8d2177adf8b4) (device:SM-N9006) Language/zh_CN com.chaoxing.mobile/ChaoXingStudy_3_6.4.5_android_phone_10831_263 (@Kalimdor)_6e98c17c08204755905e1537b1933e72",
'Accept': "application/json, text/javascript, */*; q=0.01",
'X-Requested-With': "XMLHttpRequest",
'Referer': window.location.href
},
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
logger.verbose && logger.verbose(`人脸完整响应: ${response.responseText}`);
if (data && data.code === 0 && data.status === true && data.msg === "通过") {
logger.info("人脸成功!");
showCustomNotification("人脸签到", "人脸验证成功完成", "success");
globalState.isProcessing = false;
} else if (data && data.code === -4) {
logger.error(`权限错误: ${data.msg || "你没有权限"}`);
showCustomNotification("人脸识别失败", "权限不足,请重新登录", "error");
globalState.isProcessing = false;
} else {
logger.error(`人脸失败: ${data?.message || data?.msg || "未知错误"} (重试次数: ${retryCount})`);
// 特殊处理"用户图片信息出错"
if (data?.msg === "用户图片信息出错") {
logger.info('检测到图片信息错误,将重新获取和处理图片');
showCustomNotification("图片错误", "图片信息有误,正在重新处理...", "warning");
} else {
showCustomNotification("人脸识别失败", data?.message || data?.msg || "未知错误", "error");
}
handleRetry(params, retryCount);
}
} catch (e) {
if (response.responseText.includes("网页解析失败")) {
logger.error(`服务器返回解析失败,可能URL或参数错误`);
showCustomNotification("人脸识别失败", "服务器解析失败,URL或参数可能有误", "warning");
globalState.isProcessing = false;
} else {
logger.error(`解析人脸响应失败: ${e.message} (重试次数: ${retryCount})`);
showCustomNotification("人脸识别失败", "响应解析错误,请查看控制台", "error");
handleRetry(params, retryCount);
}
}
},
onerror: function(error) {
logger.error(`人脸请求失败: ${error} (重试次数: ${retryCount})`);
showCustomNotification("人脸请求失败", `网络错误: ${error}`, "error");
handleRetry(params, retryCount);
}
});
// 修改重试逻辑:保持参数不变,只重新获取图片和objectId
async function handleRetry(originalParams, retryCount) {
if (retryCount < maxRetries) {
logger.info(`${retryDelay/1000}秒后进行第${retryCount + 1}次重试,将重新获取图片和objectId`);
setTimeout(async () => {
try {
// 获取用户ID
const puid = session.getCookie('_uid');
if (!puid) {
throw new Error('未获取到用户ID,请重新登录');
}
// 重新获取Token
logger.info('重新获取上传Token...');
const uploadToken = await getUserTokenPromise();
// 重新获取人脸图片
logger.info('重新获取人脸图片数据...');
const imgBytes = await getUserFaceImageDataPromise(puid);
// 重新处理图片
logger.info('重新处理人脸图片...');
const processedBytes = await processImageDataPromise(imgBytes);
// 重新上传图片
logger.info('重新上传人脸图片...');
const newObjectId = await uploadFaceImagePromise(processedBytes, puid, uploadToken);
// 使用原始参数和新的objectId重新提交
logger.info('使用新的objectId重新提交人脸识别...');
submitSign(originalParams, newObjectId, retryCount + 1);
} catch (error) {
logger.error(`重试过程失败: ${error.message}`);
showCustomNotification('重试失败', error.message, 'error');
// 如果重试过程失败,继续下一次重试
if (retryCount + 1 < maxRetries) {
handleRetry(originalParams, retryCount + 1);
} else {
logger.error("已达到最大重试次数,人脸识别失败");
showCustomNotification("人脸识别失败", "已达到最大重试次数", "error");
globalState.isProcessing = false;
}
}
}, retryDelay);
} else {
logger.error("已达到最大重试次数,人脸识别失败");
showCustomNotification("人脸识别失败", "已达到最大重试次数", "error");
globalState.isProcessing = false;
}
}
}
// 修改autoProcessFaceRecognition函数,增加参数缓存
async function autoProcessFaceRecognition() {
if (globalState.isProcessing) {
logger.info('人脸识别正在处理中,跳过本次调用');
return;
}
globalState.isProcessing = true;
logger.info('开始自动处理人脸识别');
try {
// 1. 初始化Cookie
initCookies();
const puid = session.getCookie('_uid');
if (!puid) {
throw new Error('未获取到用户ID,请重新登录');
}
// 2. 提取参数
const extractor = new FaceQRExtractor();
let params = extractor.getAllParams();
if (!params) {
// 尝试从缓存中获取参数
try {
const cachedParams = localStorage.getItem('lastValidParams');
if (cachedParams) {
params = JSON.parse(cachedParams);
logger.info('使用缓存的参数');
}
} catch (e) {
logger.error('获取缓存参数失败: ' + e.message);
}
if (!params) {
logger.info('首次参数提取失败,等待2秒后重试');
await new Promise(resolve => setTimeout(resolve, 2000));
params = extractor.getAllParams();
if (!params) {
throw new Error('未能提取到有效的QR码参数');
}
}
}
// 缓存有效参数
try {
localStorage.setItem('lastValidParams', JSON.stringify(params));
} catch (e) {
logger.error('缓存参数失败: ' + e.message);
}
// 3. 验证必要参数
const requiredParams = ['courseid', 'clazzid', 'cpi', 'uuid'];
const missingParams = requiredParams.filter(param => !params[param]);
if (missingParams.length > 0) {
throw new Error(`缺少必要参数: ${missingParams.join(', ')}`);
}
// 4. 获取Token
logger.info('获取上传Token...');
const uploadToken = await getUserTokenPromise();
// 5. 获取人脸图片
logger.info('获取人脸图片数据...');
const imgBytes = await getUserFaceImageDataPromise(puid);
// 6. 处理图片
logger.info('处理人脸图片...');
const processedBytes = await processImageDataPromise(imgBytes);
// 7. 上传图片
logger.info('上传人脸图片...');
const objectId = await uploadFaceImagePromise(processedBytes, puid, uploadToken);
// 8. 提交人脸识别
logger.info('提交人脸识别...');
submitSign(params, objectId);
} catch (error) {
logger.error('人脸识别处理失败: ' + error.message);
showCustomNotification('人脸识别失败', error.message, 'error');
globalState.isProcessing = false;
}
}
// 创建控制面板
function createControlPanel() {
if (document.getElementById('faceRecognitionPanel')) {
return; // 防止重复创建
}
const panel = document.createElement('div');
panel.id = 'faceRecognitionPanel';
panel.style.cssText = `
position: fixed;
top: 10px;
left: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 12px;
padding: 20px;
z-index: 10000;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
min-width: 320px;
backdrop-filter: blur(10px);
cursor: move;
user-select: none;
transition: all 0.3s ease;
`;
panel.innerHTML = `
${JSON.stringify(params, null, 2)}`; display.style.display = 'block'; showCustomNotification('✅ 提取成功', '参数已显示在控制面板中', 'success'); } else { display.innerHTML = '