// ==UserScript== // @name 不智慧教室 // @version 1.0 // @description Bypass CORS in private // @author singledog // @match https://duaa.singledog233.top/* // @grant GM_xmlhttpRequest // @connect iclass.buaa.edu.cn // @run-at document-start // @license MIT // @namespace https://greasyfork.org/users/1226768 // @downloadURL none // ==/UserScript== (function () { 'use strict'; const DEBUG = false; const dlog = (...args) => { if (DEBUG) console.log('[iClass-Userscript]', ...args); }; dlog('Userscript loaded at', location.href); // 简单的内存缓存:按 studentId 存最近一次查询到的课程数组 // 形如 { [studentId]: [{ id, classBeginTime, classEndTime, ...}, ...] } const scheduleCache = new Map(); function cacheSchedule(studentId, scheduleRes) { try { const list = Array.isArray(scheduleRes?.result) ? scheduleRes.result : []; if (studentId && list.length) { scheduleCache.set(String(studentId), list); dlog('cacheSchedule:stored', { studentId, count: list.length }); } } catch (_) { /* ignore */ } } function findCourseTimes(studentId, courseSchedId) { try { const list = scheduleCache.get(String(studentId)); if (!list || !list.length) return null; const item = list.find(it => String(it?.id) === String(courseSchedId)); if (!item) return null; const beginStr = item.classBeginTime; // 例如: "2025-10-21 09:50:00" const endStr = item.classEndTime; // 例如: "2025-10-21 11:25:00" if (!beginStr || !endStr) return null; const begin = new Date(beginStr); const end = new Date(endStr); if (isNaN(begin.getTime()) || isNaN(end.getTime())) return null; return { begin, end }; } catch (_) { return null; } } function randomTimestampBetween(begin, end) { // 在上课过程中选一个更自然的时间:开始+5分钟 ~ 结束-1分钟 const marginStartMs = 5 * 60 * 1000; const marginEndMs = 1 * 60 * 1000; const startMs = begin.getTime() + marginStartMs; const endMs = end.getTime() - marginEndMs; if (endMs <= startMs) return null; // 边界异常时放弃 const rnd = startMs + Math.floor(Math.random() * (endMs - startMs + 1)); return rnd; } function httpRequest(method, url, { headers = {}, data = null, timeout = 8000 } = {}) { dlog('httpRequest:start', { method, url, headers, dataPreview: (typeof data === 'string' ? data.slice(0, 128) : data), timeout }); return new Promise((resolve, reject) => { const t0 = performance.now(); GM_xmlhttpRequest({ method, url, headers, data, timeout, onload: (res) => { const dt = (performance.now() - t0).toFixed(1); dlog('httpRequest:onload', { url, status: res.status, timeMs: dt, length: (res.responseText || '').length }); try { const json = JSON.parse(res.responseText || '{}'); dlog('httpRequest:json', json); resolve(json); } catch (e) { dlog('httpRequest:parse-error', e); resolve({ STATUS: '1', message: '响应非JSON', raw: res.responseText }); } }, onerror: (e) => { dlog('httpRequest:onerror', e); reject(new Error('网络错误')); }, ontimeout: () => { dlog('httpRequest:timeout', { url, timeout }); reject(new Error('请求超时')); }, }); }); } function toQuery(params) { const usp = new URLSearchParams(); Object.entries(params).forEach(([k, v]) => usp.append(k, v)); return usp.toString(); } // 登录 async function login(studentId) { dlog('login:start', { studentId }); const url = `https://iclass.buaa.edu.cn:8346/app/user/login.action?` + toQuery({ password: '', phone: studentId, userLevel: '1', verificationType: '2', verificationUrl: '', }); const res = await httpRequest('GET', url); dlog('login:done', res); return res; } // 课表查询 async function getSchedule(userId, sessionId, dateStr) { dlog('getSchedule:start', { userId, sessionIdPreview: (sessionId || '').slice(0, 6) + '...', dateStr }); const url = `https://iclass.buaa.edu.cn:8346/app/course/get_stu_course_sched.action?` + toQuery({ dateStr, id: userId, }); const res = await httpRequest('GET', url, { headers: { sessionId } }); dlog('getSchedule:done', res); return res; } // 签到 async function sign(userId, courseSchedId, tsOverride) { dlog('sign:start', { userId, courseSchedId }); const tsMs = (typeof tsOverride === 'number' && tsOverride > 0) ? tsOverride : Date.now(); const url = `http://iclass.buaa.edu.cn:8081/app/course/stu_scan_sign.action?courseSchedId=${encodeURIComponent(courseSchedId)}×tamp=${tsMs}`; const body = toQuery({ id: userId }); // 表单方式 const res = await httpRequest('POST', url, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, data: body, }); dlog('sign:done', res); return res; } // 使用 postMessage 监听来自页面的请求,并回传处理结果 window.addEventListener('message', async (ev) => { try { const data = ev.data || {}; if (!data.__ICLASS_MSG__) return; const { type, id, payload } = data; dlog('message:received', { type, id, payload }); if (type === 'iclass:intranet:query') { const { studentId, dateStr } = payload || {}; try { const loginRes = await login(studentId); if (loginRes.STATUS !== '0') { dlog('query:login-failed', loginRes); window.postMessage({ __ICLASS_MSG__: true, type: 'iclass:intranet:query:result', id, payload: { login: loginRes, schedule: { STATUS: '1', message: '登录失败' } } }, '*'); return; } const userId = loginRes.result?.id; const sessionId = loginRes.result?.sessionId; const scheduleRes = await getSchedule(userId, sessionId, dateStr); // 缓存课表,供后续签到时生成课程内随机时间戳使用 cacheSchedule(studentId, scheduleRes); dlog('query:success'); window.postMessage({ __ICLASS_MSG__: true, type: 'iclass:intranet:query:result', id, payload: { login: loginRes, schedule: scheduleRes } }, '*'); } catch (e) { dlog('query:error', e); window.postMessage({ __ICLASS_MSG__: true, type: 'iclass:intranet:query:result', id, payload: { login: { STATUS: '1', message: e.message }, schedule: { STATUS: '1', message: e.message } } }, '*'); } } if (type === 'iclass:intranet:signin') { const { studentId, courseSchedId } = payload || {}; try { const loginRes = await login(studentId); if (loginRes.STATUS !== '0') { dlog('signin:login-failed', loginRes); window.postMessage({ __ICLASS_MSG__: true, type: 'iclass:intranet:signin:result', id, payload: { STATUS: '1', message: '登录失败' } }, '*'); return; } const userId = loginRes.result?.id; // 若能从缓存课表中找到对应课程时间,则将 URL 的 timestamp 改为课程进行中的随机时间;否则不改动 let tsOverride = undefined; const times = findCourseTimes(studentId, courseSchedId); if (times) { const rnd = randomTimestampBetween(times.begin, times.end); if (typeof rnd === 'number' && rnd > 0) { tsOverride = rnd; dlog('signin:timestamp-override', { begin: times.begin.toISOString(), end: times.end.toISOString(), ts: tsOverride }); } } const signRes = await sign(userId, courseSchedId, tsOverride); dlog('signin:success'); window.postMessage({ __ICLASS_MSG__: true, type: 'iclass:intranet:signin:result', id, payload: signRes }, '*'); } catch (e) { dlog('signin:error', e); window.postMessage({ __ICLASS_MSG__: true, type: 'iclass:intranet:signin:result', id, payload: { STATUS: '1', message: e.message } }, '*'); } } } catch (err) { dlog('message:handler-error', err); } }); })();