// ==UserScript==
// @name 雨课堂刷课助手
// @namespace http://tampermonkey.net/
// @version 2.3.1
// @description 针对雨课堂视频进行自动播放,配置题库自动答题
// @author 叶屿
// @license GPL3
// @antifeature payment 题库答题功能需要验证码(免费)或激活码(付费),视频播放等基础功能完全免费
// @match *://*.yuketang.cn/*
// @match *://*.gdufemooc.cn/*
// @match *://*exam.yuketang.cn/*
// @match *://*-exam.yuketang.cn/*
// @run-at document-start
// @icon http://yuketang.cn/favicon.ico
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @connect qsy.iano.cn
// @connect lyck6.cn
// @connect cdn.jsdelivr.net
// @connect unpkg.com
// @connect open.bigmodel.cn
// @connect dashscope.aliyuncs.com
// @connect ark.cn-beijing.volces.com
// @connect spark-api-open.xf-yun.com
// @connect aip.baidubce.com
// @connect api.deepseek.com
// @connect api.moonshot.cn
// @connect api.openai.com
// @connect generativelanguage.googleapis.com
// @connect api.groq.com
// @connect api.siliconflow.cn
// @connect api.lingyiwanwu.com
// @connect api.minimax.chat
// @connect api.stepfun.com
// @connect api.baichuan-ai.com
// @connect yuketang.cn
// @connect gdufemooc.cn
// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @require https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js
// @require https://cdn.jsdelivr.net/npm/opentype.js@1.3.4/dist/opentype.min.js
// ==/UserScript==
/*
* ==========================================
* 雨课堂刷课助手 v2.3.1
* ==========================================
*
* 【功能说明】
* 1. 视频自动播放(支持倍速、静音、防暂停)
* 2. 作业自动答题(OCR识别 + 题库查询)
* 3. 考试自动答题(支持停止/继续)
* 4. 双模式题库(免费验证码 / 付费激活码)
*
* 【积分购买】
* 联系微信:C919irt
* 价格表:50积分=2元,100积分=4元,150积分=6元,200积分=8元,500积分=18元
* 说明:每次答题消耗1积分,积分永久有效
*
* 【付费声明】
* 本脚本基础功能(视频播放、进度保存)完全免费
* 题库答题功能需要验证码(免费24小时)或激活码(付费永久)
* 付费仅用于题库API调用成本,不强制购买
*
* 【免责声明】
* 本脚本仅供学习交流使用,请勿用于违反学校规定或作弊行为
* 使用本脚本造成的任何后果由使用者自行承担
*
* 【版权信息】
* 作者:叶屿 | 版本:v2.3.1 | 更新:2026-04-26
*
* ==========================================
*/
(() => {
'use strict';
let panel; // UI 面板实例后置初始化
let isRunning = false; // 标记是否正在运行
let stopRequested = false; // 标记是否请求停止
let AI_PROVIDERS = {};
// ---- 脚本配置,用户可修改 ----
const Config = {
version: '2.3.1', // 版本号
playbackRate: 2, // 视频播放倍速
pptInterval: 3000, // ppt翻页间隔
storageKeys: { // 使用者勿动
progress: '[雨课堂脚本]刷课进度信息',
deviceId: 'ykt_device_id',
activationCode: 'ykt_activation_code',
answerMode: 'ykt_answer_mode', // 答题模式:free/paid
verifyValidUntil: 'ykt_verify_valid_until', // 验证码有效期
proClassCount: 'pro_lms_classCount',
feature: 'ykt_feature_conf', // 是否开启题库作答/自动评论
runState: 'ykt_run_state',
aiApiKey: 'ykt_ai_api_key',
aiProvider: 'ykt_ai_provider',
aiUnlockCode: 'ykt_ai_unlock_code',
aiUnlockUntil: 'ykt_ai_unlock_until',
playbackRate: 'ykt_playback_rate',
unmatchedFallback: 'ykt_unmatched_fallback'
}
};
const Utils = {
// 短暂睡眠,等待网页加载
sleep: (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms)),
// 将一个 JSON 字符串解析为 JavaScript 对象
safeJSONParse(value, fallback) {
try {
return JSON.parse(value);
} catch (_) {
return fallback;
}
},
// 每隔一段时间检查某个条件是否满足(通过 checker 函数),如果满足就成功返回;如果超时仍未满足,就失败返回
poll(checker, { interval = 1000, timeout = 20000 } = {}) {
return new Promise(resolve => {
const start = Date.now();
const timer = setInterval(() => {
if (checker()) {
clearInterval(timer);
resolve(true);
return;
}
if (Date.now() - start > timeout) {
clearInterval(timer);
resolve(false);
}
}, interval);
});
},
// 使用UI课程完成度来判别是否完成课程
isProgressDone(text) {
if (!text) return false;
return text.includes('100%') || text.includes('99%') || text.includes('98%') || text.includes('已完成');
},
// 主要是规避firefox会创建多个iframe的问题
inIframe() {
return window.top !== window.self;
},
// 下滑到最底部,触发课程加载
scrollToBottom(containerSelector) {
const el = document.querySelector(containerSelector);
if (el) el.scrollTop = el.scrollHeight;
},
async getDDL() {
const element = document.querySelector('video') || document.querySelector('audio');
const fallback = 180_000;
if (!element) return fallback;
let duration = Number(element.duration);
if (!Number.isFinite(duration) || duration <= 0) {
await new Promise(resolve => element.addEventListener('loadedmetadata', resolve, { once: true }));
duration = Number(element.duration);
}
const elementDurationMs = duration * 1000; // 转为秒
const timeout = Math.max(elementDurationMs * 3, 10_000); // 至少 10 秒(防极短视频);
return timeout;
}
};
// ---- 存储工具 ----
const Store = {
getProgress(url) {
const raw = localStorage.getItem(Config.storageKeys.progress);
const all = Utils.safeJSONParse(raw, {}) || { url: { outside: 0, inside: 0 } };
if (!all[url]) {
all[url] = { outside: 0, inside: 0 };
localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all));
}
return { all, current: all[url] };
},
setProgress(url, outside, inside = 0) {
const raw = localStorage.getItem(Config.storageKeys.progress);
const all = Utils.safeJSONParse(raw, {});
all[url] = { outside, inside };
localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all));
},
removeProgress(url) {
const raw = localStorage.getItem(Config.storageKeys.progress);
const all = Utils.safeJSONParse(raw, {});
delete all[url];
localStorage.setItem(Config.storageKeys.progress, JSON.stringify(all));
},
getDeviceId() {
let deviceId = localStorage.getItem(Config.storageKeys.deviceId);
if (!deviceId) {
deviceId = 'ykt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem(Config.storageKeys.deviceId, deviceId);
}
return deviceId;
},
getActivationCode() {
return localStorage.getItem(Config.storageKeys.activationCode) || '';
},
setActivationCode(code) {
localStorage.setItem(Config.storageKeys.activationCode, code);
},
getAnswerMode() {
return localStorage.getItem(Config.storageKeys.answerMode) || 'free';
},
setAnswerMode(mode) {
localStorage.setItem(Config.storageKeys.answerMode, mode);
},
getVerifyValidUntil() {
return Number(localStorage.getItem(Config.storageKeys.verifyValidUntil)) || 0;
},
setVerifyValidUntil(timestamp) {
localStorage.setItem(Config.storageKeys.verifyValidUntil, timestamp);
},
isVerifyValid() {
const validUntil = this.getVerifyValidUntil();
return validUntil > Date.now() / 1000;
},
getProClassCount() {
const value = localStorage.getItem(Config.storageKeys.proClassCount);
return value ? Number(value) : 1;
},
setProClassCount(count) {
localStorage.setItem(Config.storageKeys.proClassCount, count);
},
getFeatureConf() {
const raw = localStorage.getItem(Config.storageKeys.feature);
const saved = Utils.safeJSONParse(raw, {}) || {};
const conf = {
autoAI: saved.autoAI ?? false,
autoComment: saved.autoComment ?? false,
commentMode: saved.commentMode ?? 'copy', // 'copy' = 复制他人评论, 'ai' = AI生成评论
skipLive: saved.skipLive ?? false,
};
localStorage.setItem(Config.storageKeys.feature, JSON.stringify(conf));
return conf;
},
setFeatureConf(conf) {
localStorage.setItem(Config.storageKeys.feature, JSON.stringify(conf));
},
getRunState() {
const raw = localStorage.getItem(Config.storageKeys.runState);
return Utils.safeJSONParse(raw, null);
},
setRunState(status) {
const state = { status, lastActiveTime: Date.now() };
localStorage.setItem(Config.storageKeys.runState, JSON.stringify(state));
},
clearRunState() {
localStorage.removeItem(Config.storageKeys.runState);
},
touchRunState() {
const state = this.getRunState();
if (state && state.status === 'running') {
state.lastActiveTime = Date.now();
localStorage.setItem(Config.storageKeys.runState, JSON.stringify(state));
}
},
getAIApiKey() {
return localStorage.getItem(Config.storageKeys.aiApiKey) || '';
},
setAIApiKey(key) {
localStorage.setItem(Config.storageKeys.aiApiKey, key);
},
getAIProvider() {
return localStorage.getItem(Config.storageKeys.aiProvider) || 'zhipu';
},
setAIProvider(provider) {
localStorage.setItem(Config.storageKeys.aiProvider, provider);
},
getAIUnlockCode() {
return localStorage.getItem(Config.storageKeys.aiUnlockCode) || '';
},
setAIUnlockCode(code) {
localStorage.setItem(Config.storageKeys.aiUnlockCode, code);
},
getAIUnlockUntil() {
return Number(localStorage.getItem(Config.storageKeys.aiUnlockUntil)) || 0;
},
setAIUnlockUntil(timestamp) {
localStorage.setItem(Config.storageKeys.aiUnlockUntil, timestamp);
},
isAIUnlocked() {
const until = this.getAIUnlockUntil();
return until > Date.now() / 1000;
},
getPlaybackRate() {
return Number(localStorage.getItem(Config.storageKeys.playbackRate)) || 2;
},
setPlaybackRate(rate) {
localStorage.setItem(Config.storageKeys.playbackRate, rate);
Config.playbackRate = rate;
},
getUnmatchedFallback() {
// 题库未匹配时的兜底策略:ai(调用AI,按题型支持多选) | skip(跳过)
return localStorage.getItem(Config.storageKeys.unmatchedFallback) || 'ai';
},
setUnmatchedFallback(strategy) {
localStorage.setItem(Config.storageKeys.unmatchedFallback, strategy);
}
};
// ---- UI 面板 ----
function createPanel() {
const iframe = document.createElement('iframe');
iframe.style.position = 'fixed';
iframe.style.top = '40px';
iframe.style.left = '40px';
iframe.style.width = '480px';
iframe.style.height = '520px';
iframe.style.zIndex = '999999';
iframe.style.border = '1px solid #a3a3a3';
iframe.style.borderRadius = '12px';
iframe.style.background = '#fff';
iframe.style.overflow = 'hidden';
iframe.style.boxShadow = '0 10px 40px rgba(0,0,0,0.2)';
iframe.setAttribute('frameborder', '0');
iframe.setAttribute('id', 'ykt-helper-iframe');
iframe.setAttribute('allowtransparency', 'true');
document.body.appendChild(iframe);
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
⚠️ 有BUG及时反馈需提供账号
联系微信:C919irt 📋
- ✅ 脚本已加载 v2.3.1
- 📖 题库自动答题模式
- ⚠️ 自动答题需要配置激活
- 💡 点击【题库配置】开始
- 🚀 雨课堂助手已启动
`);
doc.close();
const ui = {
iframe,
doc,
panel: doc.getElementById('panel'),
header: doc.getElementById('header'),
info: doc.getElementById('info'),
btnStart: doc.getElementById('btn-start'),
btnClear: doc.getElementById('btn-clear'),
btnSetting: doc.getElementById('btn-setting'),
settings: doc.getElementById('settings'),
saveSettings: doc.getElementById('save_settings'),
closeSettings: doc.getElementById('close_settings'),
modePaid: doc.getElementById('mode-paid'),
modeFree: doc.getElementById('mode-free'),
modeAI: doc.getElementById('mode-ai'),
aiConfigSection: doc.getElementById('ai-config-section'),
aiApiKeyInput: doc.getElementById('ai_api_key'),
aiProviderSelect: doc.getElementById('ai_provider'),
unmatchedFallback: doc.getElementById('unmatched_fallback'),
aiProviderInfo: doc.getElementById('ai_provider_info'),
aiUnlockCode: doc.getElementById('ai_unlock_code'),
aiUnlockBtn: doc.getElementById('ai_unlock_btn'),
aiUnlockStatus: doc.getElementById('ai_unlock_status'),
activationCodeSection: doc.getElementById('activation-code-section'),
activationCodeInput: doc.getElementById('activation_code'),
activateBtn: doc.getElementById('activate_btn'),
activateStatus: doc.getElementById('activate_status'),
creditsDisplay: doc.getElementById('credits_display'),
wechatCopy: doc.getElementById('wechat_copy'),
feedbackWechat: doc.getElementById('feedback_wechat'),
verifyCodeSection: doc.getElementById('verify-code-section'),
verifyCodeInput: doc.getElementById('verify_code'),
verifyBtn: doc.getElementById('verify_btn'),
verifyStatus: doc.getElementById('verify_status'),
featureAutoAI: doc.getElementById('feature_auto_ai'),
featureAutoComment: doc.getElementById('feature_auto_comment'),
commentModeCopy: doc.getElementById('comment_mode_copy'),
commentModeAI: doc.getElementById('comment_mode_ai'),
commentModeSection: doc.getElementById('comment_mode_section'),
featureSkipLive: doc.getElementById('feature_skip_live'),
playbackRate: doc.getElementById('playback_rate'),
minimality: doc.getElementById('minimality'),
question: doc.getElementById('question'),
miniBasic: doc.getElementById('mini-basic'),
miniIcon: doc.getElementById('mini-icon'),
miniText: doc.getElementById('mini-text')
};
let isDragging = false;
let startX = 0, startY = 0, startLeft = 0, startTop = 0;
const hostWindow = window.parent || window;
const onMove = e => {
if (!isDragging) return;
const deltaX = e.screenX - startX;
const deltaY = e.screenY - startY;
const maxLeft = Math.max(0, hostWindow.innerWidth - iframe.offsetWidth);
const maxTop = Math.max(0, hostWindow.innerHeight - iframe.offsetHeight);
iframe.style.left = Math.min(Math.max(0, startLeft + deltaX), maxLeft) + 'px';
iframe.style.top = Math.min(Math.max(0, startTop + deltaY), maxTop) + 'px';
};
const stopDrag = () => {
if (!isDragging) return;
isDragging = false;
iframe.style.transition = '';
doc.body.style.userSelect = '';
};
// 标题栏拖拽
ui.header.addEventListener('mousedown', e => {
isDragging = true;
startX = e.screenX;
startY = e.screenY;
startLeft = parseFloat(iframe.style.left) || 0;
startTop = parseFloat(iframe.style.top) || 0;
iframe.style.transition = 'none';
doc.body.style.userSelect = 'none';
e.preventDefault();
});
// 缩小按钮拖拽
ui.miniBasic.addEventListener('mousedown', e => {
isDragging = true;
startX = e.screenX;
startY = e.screenY;
startLeft = parseFloat(iframe.style.left) || 0;
startTop = parseFloat(iframe.style.top) || 0;
iframe.style.transition = 'none';
doc.body.style.userSelect = 'none';
e.preventDefault();
e.stopPropagation(); // 防止触发点击事件
});
doc.addEventListener('mousemove', onMove);
hostWindow.addEventListener('mousemove', onMove);
doc.addEventListener('mouseup', stopDrag);
hostWindow.addEventListener('mouseup', stopDrag);
hostWindow.addEventListener('blur', stopDrag);
const normalSize = { width: parseFloat(iframe.style.width), height: parseFloat(iframe.style.height) };
const miniSize = 64;
let isMinimized = false;
const enterMini = () => {
if (isMinimized) return;
isMinimized = true;
ui.panel.style.display = 'none';
ui.miniBasic.classList.add('show');
iframe.style.width = '80px';
iframe.style.height = '64px';
};
const exitMini = () => {
if (!isMinimized) return;
isMinimized = false;
ui.panel.style.display = '';
ui.miniBasic.classList.remove('show');
iframe.style.width = normalSize.width + 'px';
iframe.style.height = normalSize.height + 'px';
};
ui.minimality.addEventListener('click', enterMini);
ui.miniBasic.addEventListener('click', exitMini);
ui.question.addEventListener('click', () => {
window.parent.alert('雨课堂助手 v2.3.1\n作者:叶屿\n\n功能说明:\n- 自动播放视频/音频\n- 题库自动答题\n- AI智能答题(14种大模型可选)\n- AI限速解锁(验证码24h极速答题)\n- AI智能评论/自动回复\n- 自动评论回复');
});
const log = message => {
const li = doc.createElement('li');
li.innerText = message;
ui.info.appendChild(li);
// 限制最多 100 条日志,防止课程内容多时爆内存
while (ui.info.children.length > 100) {
ui.info.removeChild(ui.info.firstChild);
}
if (ui.info.lastElementChild) ui.info.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
};
// 查询积分
const queryCredits = async () => {
const deviceId = Store.getDeviceId();
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://qsy.iano.cn/index.php?s=/api/question_bank/credits&device_id=${deviceId}`,
timeout: 15000,
onload: res => {
if (res.status === 200) {
try {
const json = JSON.parse(res.responseText);
if (json.code === 1) {
resolve(json.data);
} else {
reject(json.msg || '查询失败');
}
} catch (e) {
reject('JSON 解析失败');
}
} else {
reject(`请求失败: HTTP ${res.status}`);
}
},
onerror: () => reject('网络错误'),
ontimeout: () => reject('请求超时')
});
});
};
// 更新积分显示
const updateCreditsDisplay = async () => {
try {
const data = await queryCredits();
ui.creditsDisplay.textContent = `${data.remaining_credits} 积分`;
} catch (err) {
ui.creditsDisplay.textContent = '-- 积分';
}
};
// 验证验证码
const verifyCode = async (code) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://qsy.iano.cn/index.php?s=/api/code/verify',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: 'code=' + encodeURIComponent(code),
timeout: 15000,
onload: res => {
if (res.status === 200) {
try {
const json = JSON.parse(res.responseText);
if (json.code === 1 && json.data.valid) {
resolve(json);
} else {
reject(json.msg || '验证码无效或已过期');
}
} catch (e) {
reject('JSON 解析失败');
}
} else {
reject(`请求失败: HTTP ${res.status}`);
}
},
onerror: () => reject('网络错误'),
ontimeout: () => reject('请求超时')
});
});
};
// 激活题库激活码
const activateCode = async (code, deviceId) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/activate',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
code: code,
device_id: deviceId
}),
timeout: 15000,
onload: res => {
if (res.status === 200) {
try {
const json = JSON.parse(res.responseText);
if (json.code === 1) {
resolve(json);
} else {
reject(json.msg || '激活失败');
}
} catch (e) {
reject('JSON 解析失败');
}
} else {
reject(`请求失败: HTTP ${res.status}`);
}
},
onerror: () => reject('网络错误'),
ontimeout: () => reject('请求超时')
});
});
};
// 切换答题模式
const switchAnswerMode = (mode) => {
Store.setAnswerMode(mode);
ui.modePaid.classList.remove('selected');
ui.modeFree.classList.remove('selected');
ui.modeAI.classList.remove('selected');
ui.activationCodeSection.style.display = 'none';
ui.verifyCodeSection.style.display = 'none';
ui.aiConfigSection.style.display = 'none';
if (mode === 'paid') {
ui.modePaid.classList.add('selected');
ui.activationCodeSection.style.display = 'block';
updateCreditsDisplay();
} else if (mode === 'ai') {
ui.modeAI.classList.add('selected');
ui.aiConfigSection.style.display = 'block';
ui.aiApiKeyInput.value = Store.getAIApiKey();
ui.aiProviderSelect.value = Store.getAIProvider();
updateAIProviderInfo();
updateAIUnlockStatus();
} else {
ui.modeFree.classList.add('selected');
ui.verifyCodeSection.style.display = 'block';
}
};
// 绑定答题模式切换事件
ui.modePaid.addEventListener('click', () => switchAnswerMode('paid'));
ui.modeFree.addEventListener('click', () => switchAnswerMode('free'));
ui.modeAI.addEventListener('click', () => switchAnswerMode('ai'));
// 绑定微信号复制
ui.wechatCopy.addEventListener('click', () => {
const wechat = 'C919irt';
const textarea = doc.createElement('textarea');
textarea.value = wechat;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
doc.body.appendChild(textarea);
textarea.select();
try {
doc.execCommand('copy');
ui.wechatCopy.textContent = '✅ 已复制';
setTimeout(() => {
ui.wechatCopy.textContent = 'C919irt 📋';
}, 2000);
} catch (err) {
console.error('复制失败', err);
}
doc.body.removeChild(textarea);
});
// 绑定反馈微信号复制
ui.feedbackWechat.addEventListener('click', () => {
const wechat = 'C919irt';
const textarea = doc.createElement('textarea');
textarea.value = wechat;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
doc.body.appendChild(textarea);
textarea.select();
try {
doc.execCommand('copy');
ui.feedbackWechat.textContent = '✅ 已复制';
setTimeout(() => {
ui.feedbackWechat.textContent = 'C919irt 📋';
}, 2000);
} catch (err) {
console.error('复制失败', err);
}
doc.body.removeChild(textarea);
});
// 绑定激活按钮
ui.activateBtn.addEventListener('click', async () => {
const code = ui.activationCodeInput.value.trim();
if (!code) {
ui.activateStatus.className = 'verify-status error';
ui.activateStatus.textContent = '❌ 请输入激活码';
ui.activateStatus.style.display = 'block';
return;
}
ui.activateBtn.disabled = true;
ui.activateBtn.textContent = '激活中...';
ui.activateStatus.style.display = 'none';
try {
const deviceId = Store.getDeviceId();
const result = await activateCode(code, deviceId);
Store.setActivationCode(code);
ui.activateStatus.className = 'verify-status success';
ui.activateStatus.textContent = `✅ 激活成功!获得 ${result.data.credits} 积分`;
ui.activateStatus.style.display = 'block';
ui.activationCodeInput.value = '';
await updateCreditsDisplay(); // 更新积分显示
log(`✅ 激活成功!获得 ${result.data.credits} 积分`);
} catch (err) {
ui.activateStatus.className = 'verify-status error';
ui.activateStatus.textContent = `❌ ${err}`;
ui.activateStatus.style.display = 'block';
} finally {
ui.activateBtn.disabled = false;
ui.activateBtn.textContent = '激活';
}
});
// 绑定验证码验证按钮
ui.verifyBtn.addEventListener('click', async () => {
const vcode = ui.verifyCodeInput.value.trim();
if (!vcode || vcode.length !== 4) {
ui.verifyStatus.className = 'verify-status error';
ui.verifyStatus.textContent = '❌ 请输入4位验证码';
ui.verifyStatus.style.display = 'block';
return;
}
ui.verifyBtn.disabled = true;
ui.verifyBtn.textContent = '验证中...';
ui.verifyStatus.style.display = 'none';
try {
const result = await verifyCode(vcode);
Store.setVerifyValidUntil(result.data.valid_until);
ui.verifyStatus.className = 'verify-status success';
ui.verifyStatus.textContent = `✅ 验证成功!有效期至 ${result.data.valid_until_str}`;
ui.verifyStatus.style.display = 'block';
ui.verifyCodeInput.value = '';
ui.verifyCodeInput.disabled = true;
log(`✅ 验证码验证成功!有效期至 ${result.data.valid_until_str}`);
} catch (err) {
ui.verifyStatus.className = 'verify-status error';
ui.verifyStatus.textContent = `❌ ${err}`;
ui.verifyStatus.style.display = 'block';
} finally {
ui.verifyBtn.disabled = false;
ui.verifyBtn.textContent = '验证';
}
});
// 支持回车键验证
ui.verifyCodeInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
ui.verifyBtn.click();
}
});
// ---- AI多模型 Provider 信息配置 ----
AI_PROVIDERS = {
zhipu: {
name: '智谱GLM-4-Flash',
model: 'glm-4-flash',
url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
free: true,
desc: '智谱AI免费模型,速度快,准确率较高',
link: 'https://www.bigmodel.cn/invite?icode=upWK8Rq09RynLhZ8CwWAqf2gad6AKpjZefIo3dVEQyA%3D',
linkText: '前往智谱AI开放平台注册/登录获取API Key →',
keyHint: '进入控制台 → API Keys → 创建API Key → 复制'
},
deepseek: {
name: 'DeepSeek-V3',
model: 'deepseek-chat',
url: 'https://api.deepseek.com/chat/completions',
free: false,
desc: 'DeepSeek高性价比模型,百万token仅需1元,答题准确率最高',
link: 'https://platform.deepseek.com/',
linkText: '前往DeepSeek开放平台注册/登录获取API Key →',
keyHint: '登录后 → 左侧API Keys → 创建API Key → 复制'
},
deepseek_v4: {
name: 'DeepSeek-V4-Pro',
model: 'deepseek-v4-pro',
url: 'https://api.deepseek.com/chat/completions',
free: false,
desc: 'DeepSeek最新旗舰模型(1.6T参数/1M上下文),答题准确率显著高于V3,限时75折',
link: 'https://platform.deepseek.com/',
linkText: '前往DeepSeek开放平台注册/登录获取API Key →',
keyHint: '登录后 → 左侧API Keys → 创建API Key → 复制(与V3共用同一Key)'
},
qwen: {
name: '通义千问Qwen-Plus',
model: 'qwen-plus',
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
free: false,
desc: '阿里云通义千问,新用户有免费额度,知识面广',
link: 'https://dashscope.console.aliyun.com/',
linkText: '前往阿里云百炼平台注册/登录获取API Key →',
keyHint: '登录后 → 右上角API-KEY → 创建API Key → 复制'
},
doubao: {
name: '豆包Doubao-lite-32k',
model: 'doubao-lite-32k',
url: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
free: true,
desc: '字节跳动豆包免费模型,速度极快',
link: 'https://console.volcengine.com/ark/',
linkText: '前往火山引擎Ark平台注册/登录获取API Key →',
keyHint: '登录 → API Key管理 → 创建API Key → 复制(需先开通模型)'
},
spark: {
name: '讯飞星火Spark Lite',
model: 'spark-lite',
url: 'https://spark-api-open.xf-yun.com/v1/chat/completions',
free: true,
desc: '科大讯飞星火免费模型,中文理解能力强',
link: 'https://console.xfyun.cn/',
linkText: '前往讯飞开放平台注册/登录获取API Key →',
keyHint: '登录 → 控制台 → 星火大模型 → 创建应用 → 获取APIKey和APISecret'
},
kimi: {
name: 'Kimi/Moonshot',
model: 'moonshot-v1-8k',
url: 'https://api.moonshot.cn/v1/chat/completions',
free: false,
desc: 'Moonshot Kimi模型,长文本理解能力强,新用户有免费额度',
link: 'https://platform.moonshot.cn/',
linkText: '前往Moonshot开放平台注册/登录获取API Key →',
keyHint: '登录 → 左侧API Key管理 → 新建API Key → 复制'
},
gpt: {
name: 'ChatGPT (GPT-4o-mini)',
model: 'gpt-4o-mini',
url: 'https://api.openai.com/v1/chat/completions',
free: false,
desc: 'OpenAI GPT-4o-mini,全球最流行AI,综合能力强,需科学上网',
link: 'https://platform.openai.com/api-keys',
linkText: '前往OpenAI平台注册/登录获取API Key →',
keyHint: '登录 → API Keys → Create new secret key → 复制(需科学上网)'
},
gemini: {
name: 'Google Gemini Flash',
model: 'gemini-2.0-flash',
url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
free: true,
desc: 'Google Gemini 2.0 Flash免费模型,速度快,多语言能力强,需科学上网',
link: 'https://aistudio.google.com/apikey',
linkText: '前往Google AI Studio获取API Key →',
keyHint: '登录Google账号 → 点击Get API Key → Create → 复制(需科学上网)'
},
groq: {
name: 'Groq (Llama 3.3 70B)',
model: 'llama-3.3-70b-versatile',
url: 'https://api.groq.com/openai/v1/chat/completions',
free: true,
desc: 'Groq超快推理引擎,免费使用Llama 3.3 70B,速度全网最快,需科学上网',
link: 'https://console.groq.com/keys',
linkText: '前往Groq控制台注册/登录获取API Key →',
keyHint: '登录 → API Keys → Create API Key → 复制(需科学上网)'
},
siliconflow: {
name: '硅基流动 (Qwen3-8B)',
model: 'Qwen/Qwen3-8B',
url: 'https://api.siliconflow.cn/v1/chat/completions',
free: true,
desc: '硅基流动聚合平台,免费调用多种开源模型,国内直连无需科学上网',
link: 'https://cloud.siliconflow.cn/',
linkText: '前往硅基流动平台注册/登录获取API Key →',
keyHint: '登录 → 左侧API密钥 → 创建API Key → 复制'
},
yi: {
name: '零一万物 (Yi-Lightning)',
model: 'yi-lightning',
url: 'https://api.lingyiwanwu.com/v1/chat/completions',
free: false,
desc: '零一万物Yi系列模型,性价比高,中英文能力出色',
link: 'https://platform.lingyiwanwu.com/',
linkText: '前往零一万物开放平台注册/登录获取API Key →',
keyHint: '登录 → API Key管理 → 创建API Key → 复制'
},
minimax: {
name: 'MiniMax (abab6.5s)',
model: 'abab6.5s-chat',
url: 'https://api.minimax.chat/v1/text/chatcompletion_v2',
free: false,
desc: 'MiniMax大模型,新用户赠送免费额度,对话能力强',
link: 'https://platform.minimaxi.com/',
linkText: '前往MiniMax开放平台注册/登录获取API Key →',
keyHint: '登录 → 接口密钥 → 创建新的密钥 → 复制'
},
stepfun: {
name: '阶跃星辰 (Step-1-Flash)',
model: 'step-1-flash',
url: 'https://api.stepfun.com/v1/chat/completions',
free: true,
desc: '阶跃星辰免费模型,推理能力强,适合复杂题目',
link: 'https://platform.stepfun.com/',
linkText: '前往阶跃星辰开放平台注册/登录获取API Key →',
keyHint: '登录 → API Key → 创建API Key → 复制'
},
baichuan: {
name: '百川智能 (Baichuan4)',
model: 'Baichuan4',
url: 'https://api.baichuan-ai.com/v1/chat/completions',
free: false,
desc: '百川智能大模型,新用户赠送免费token,中文能力优秀',
link: 'https://platform.baichuan-ai.com/',
linkText: '前往百川智能开放平台注册/登录获取API Key →',
keyHint: '登录 → API Keys → 创建API Key → 复制'
}
};
// 更新AI Provider信息显示
const updateAIProviderInfo = () => {
const provider = ui.aiProviderSelect.value;
const info = AI_PROVIDERS[provider];
if (!info) return;
const freeTag = info.free ? '免费' : '低价';
ui.aiProviderInfo.innerHTML = `
${info.desc} ${freeTag}
🔗 ${info.linkText}
💡 操作:${info.keyHint}
`;
};
// 更新AI解锁状态显示
const updateAIUnlockStatus = () => {
if (Store.isAIUnlocked()) {
const until = Store.getAIUnlockUntil();
const date = new Date(until * 1000);
const dateStr = date.toLocaleString('zh-CN');
ui.aiUnlockStatus.innerHTML = `✅ 已解锁,有效期至 ${dateStr}(答题间隔≈1.5秒)`;
ui.aiUnlockCode.disabled = true;
ui.aiUnlockBtn.disabled = true;
} else {
ui.aiUnlockStatus.innerHTML = `⏳ 未解锁,当前答题间隔35秒`;
ui.aiUnlockCode.disabled = false;
ui.aiUnlockBtn.disabled = false;
}
};
// AI Provider 切换事件
ui.aiProviderSelect.addEventListener('change', () => {
Store.setAIProvider(ui.aiProviderSelect.value);
updateAIProviderInfo();
});
// AI 解锁按钮事件
ui.aiUnlockBtn.addEventListener('click', async () => {
const code = ui.aiUnlockCode.value.trim();
if (!code || code.length !== 4) {
ui.aiUnlockStatus.innerHTML = '❌ 请输入4位解锁码';
return;
}
ui.aiUnlockBtn.disabled = true;
ui.aiUnlockBtn.textContent = '验证中...';
try {
const result = await verifyCode(code);
Store.setAIUnlockCode(code);
Store.setAIUnlockUntil(result.data.valid_until);
ui.aiUnlockCode.value = '';
updateAIUnlockStatus();
log(`🚀 AI限速已解锁!有效期至 ${result.data.valid_until_str}`);
} catch (err) {
ui.aiUnlockStatus.innerHTML = `❌ ${err}`;
} finally {
ui.aiUnlockBtn.disabled = false;
ui.aiUnlockBtn.textContent = '解锁';
}
});
// AI 解锁码回车支持
ui.aiUnlockCode.addEventListener('keypress', (e) => {
if (e.key === 'Enter') ui.aiUnlockBtn.click();
});
// 检查验证码状态
const checkVerifyStatus = () => {
if (Store.isVerifyValid()) {
const validUntil = Store.getVerifyValidUntil();
const date = new Date(validUntil * 1000);
const dateStr = date.toLocaleString('zh-CN');
ui.verifyStatus.className = 'verify-status success';
ui.verifyStatus.textContent = `✅ 已验证,有效期至 ${dateStr}`;
ui.verifyCodeInput.disabled = true;
ui.verifyBtn.disabled = true;
} else {
ui.verifyCodeInput.disabled = false;
ui.verifyBtn.disabled = false;
ui.verifyStatus.style.display = 'none';
}
};
const loadAnswerMode = () => {
const mode = Store.getAnswerMode();
switchAnswerMode(mode);
if (mode === 'free') {
checkVerifyStatus();
} else {
updateCreditsDisplay(); // 付费模式加载时查询积分
}
};
const loadActivationCode = () => {
ui.activationCodeInput.value = Store.getActivationCode();
};
const loadFeatureConf = () => {
const saved = Store.getFeatureConf();
ui.featureAutoAI.checked = saved.autoAI;
ui.featureAutoComment.checked = saved.autoComment;
ui.featureSkipLive.checked = saved.skipLive;
// 加载评论模式
if (saved.autoComment) {
ui.commentModeSection.style.display = 'block';
} else {
ui.commentModeSection.style.display = 'none';
}
const commentMode = saved.commentMode || 'copy';
if (ui.commentModeCopy) ui.commentModeCopy.checked = (commentMode === 'copy');
if (ui.commentModeAI) ui.commentModeAI.checked = (commentMode === 'ai');
// 更新AI提示
const aiHint = doc.getElementById('ai_comment_hint');
if (aiHint) aiHint.style.display = (commentMode === 'ai') ? 'block' : 'none';
};
// 评论复选框切换 → 显示/隐藏回复方式选项
ui.featureAutoComment.addEventListener('change', () => {
ui.commentModeSection.style.display = ui.featureAutoComment.checked ? 'block' : 'none';
});
// 评论模式单选切换 → 显示/隐藏AI提示
const commentModeRadios = doc.querySelectorAll('input[name="comment_mode"]');
commentModeRadios.forEach(radio => {
radio.addEventListener('change', () => {
const aiHint = doc.getElementById('ai_comment_hint');
if (aiHint) aiHint.style.display = (radio.value === 'ai' && radio.checked) ? 'block' : 'none';
});
});
loadAnswerMode();
loadActivationCode();
loadFeatureConf();
ui.btnSetting.onclick = () => {
loadAnswerMode();
loadActivationCode();
loadFeatureConf();
ui.playbackRate.value = String(Store.getPlaybackRate());
if (ui.unmatchedFallback) ui.unmatchedFallback.value = Store.getUnmatchedFallback();
ui.settings.style.display = 'block';
};
ui.closeSettings.onclick = () => {
ui.settings.style.display = 'none';
};
ui.saveSettings.onclick = async () => {
const mode = Store.getAnswerMode();
// 保存兜底策略
if (ui.unmatchedFallback) Store.setUnmatchedFallback(ui.unmatchedFallback.value);
// 保存播放倍速
const rate = parseFloat(ui.playbackRate.value) || 2;
Store.setPlaybackRate(rate);
// 保存功能配置(含评论模式)
const commentMode = (ui.commentModeAI && ui.commentModeAI.checked) ? 'ai' : 'copy';
const featureConf = {
autoAI: ui.featureAutoAI.checked,
autoComment: ui.featureAutoComment.checked,
commentMode: commentMode,
skipLive: ui.featureSkipLive.checked
};
Store.setFeatureConf(featureConf);
// 保存AI API Key
if (Store.getAnswerMode() === 'ai') {
Store.setAIApiKey(ui.aiApiKeyInput.value.trim());
}
// 检查激活状态并给出提示
if (featureConf.autoAI) {
if (mode === 'paid') {
try {
const data = await queryCredits();
if (data.remaining_credits <= 0) {
log('⚠️ 积分不足,请先购买激活码充值');
} else {
log(`✅ 题库配置已保存 (付费模式,剩余 ${data.remaining_credits} 积分)`);
}
} catch (err) {
log('⚠️ 题库配置已保存,但未检测到激活码,请先激活');
}
} else if (mode === 'ai') {
if (!Store.getAIApiKey()) {
log('⚠️ 题库配置已保存,但未填写AI API Key');
} else {
log('✅ 题库配置已保存 (AI答题模式)');
}
} else {
// 免费模式
if (!Store.isVerifyValid()) {
log('⚠️ 题库配置已保存,但验证码未验证或已过期,请先验证');
} else {
log('✅ 题库配置已保存 (免费模式)');
}
}
} else {
log('✅ 题库配置已保存');
}
ui.settings.style.display = 'none';
};
ui.btnClear.onclick = () => {
Store.removeProgress(window.parent.location.href);
localStorage.removeItem(Config.storageKeys.proClassCount);
log('已清除当前课程的刷课进度缓存');
};
// 后面赋值给panel
return {
...ui,
log,
setStartHandler(fn) {
ui.btnStart.onclick = () => {
// 如果正在运行,点击停止
if (isRunning) {
stopRequested = true;
log('⏸️ 正在停止...');
ui.btnStart.innerText = '停止中...';
ui.btnStart.disabled = true;
return;
}
log('启动中...');
isRunning = true;
stopRequested = false;
// 检查是否是考试页面
const url = location.host;
const path = location.pathname.split('/');
if (url.includes('exam.yuketang.cn') || path.includes('exam') || path.includes('exercise') || path.includes('homework')) {
ui.btnStart.innerText = '⏸️ 停止答题';
} else {
ui.btnStart.innerText = '⏸️ 停止刷课';
}
ui.btnStart.disabled = false;
Store.setRunState('running');
startMiniStatusUpdate(panel);
fn && fn();
};
},
resetStartButton(text = '开始刷课') {
ui.btnStart.innerText = text;
ui.btnStart.disabled = false;
if (text.includes('刷完') || text.includes('完成')) {
Store.setRunState('completed');
} else if (text.includes('停止') || text.includes('继续')) {
Store.setRunState('stopped');
}
isRunning = false;
stopRequested = false;
},
updateMiniStatus(icon, text) {
if (ui.miniIcon) ui.miniIcon.innerText = icon;
if (ui.miniText) ui.miniText.innerText = text;
}
};
}
// ---- 播放器工具 ----
const Player = {
applySpeed() {
const rate = Config.playbackRate;
const speedBtn = document.querySelector('xt-speedlist xt-button') || document.getElementsByTagName('xt-speedlist')[0]?.firstElementChild?.firstElementChild;
const speedWrap = document.getElementsByTagName('xt-speedbutton')[0];
if (speedBtn && speedWrap) {
speedBtn.setAttribute('data-speed', rate);
speedBtn.setAttribute('keyt', `${rate}.00`);
speedBtn.innerText = `${rate}.00X`;
const mousemove = document.createEvent('MouseEvent');
mousemove.initMouseEvent('mousemove', true, true, unsafeWindow, 0, 10, 10, 10, 10, 0, 0, 0, 0, 0, null);
speedWrap.dispatchEvent(mousemove);
speedBtn.click();
} else if (document.querySelector('video')) {
document.querySelector('video').playbackRate = rate;
}
},
mute() {
const muteBtn = document.querySelector('#video-box > div > xt-wrap > xt-controls > xt-inner > xt-volumebutton > xt-icon');
if (muteBtn) muteBtn.click();
const video = document.querySelector('video');
if (video) video.volume = 0;
},
applyMediaDefault(media) {
if (!media) return;
media.volume = 0;
media.playbackRate = Config.playbackRate;
const p = media.play();
if (p !== undefined) {
p.catch(e => {
if (e.name !== 'AbortError') console.warn('[雨课堂助手] applyMediaDefault play失败:', e);
setTimeout(() => media.play().catch(() => {}), 1500);
});
}
},
observePause(video) {
if (!video) return () => { };
const target = document.getElementsByClassName('play-btn-tip')[0];
if (!target) return () => { };
let isPlayPending = false;
// 安全播放:防止重叠的 play() 调用导致 AbortError
const safePlay = (delay = 500) => {
if (isPlayPending) return;
isPlayPending = true;
setTimeout(() => {
if (video.paused) {
const p = video.play();
if (p !== undefined) {
p.then(() => { isPlayPending = false; })
.catch(e => {
isPlayPending = false;
if (e.name !== 'AbortError') {
console.warn('[雨课堂助手] 自动播放失败:', e);
}
setTimeout(() => safePlay(1500), 1500);
});
} else {
isPlayPending = false;
}
} else {
isPlayPending = false;
}
}, delay);
};
safePlay(1500);
const observer = new MutationObserver(list => {
for (const mutation of list) {
if (mutation.type === 'childList' && target.innerText === '播放') {
safePlay(800);
}
}
});
observer.observe(target, { childList: true });
return () => observer.disconnect();
},
waitForEnd(media, timeout = 0) {
return new Promise(resolve => {
if (!media) return resolve();
if (media.ended) return resolve();
let timer;
const onEnded = () => {
clearTimeout(timer);
resolve();
};
media.addEventListener('ended', onEnded, { once: true });
if (timeout > 0) {
timer = setTimeout(() => {
media.removeEventListener('ended', onEnded);
resolve();
}, timeout);
}
});
}
};
// ---- 防切屏 ----
function preventScreenCheck() {
const win = unsafeWindow;
const blackList = new Set(['visibilitychange', 'blur', 'pagehide']);
win._addEventListener = win.addEventListener;
win.addEventListener = (...args) => blackList.has(args[0]) ? undefined : win._addEventListener(...args);
document._addEventListener = document.addEventListener;
document.addEventListener = (...args) => blackList.has(args[0]) ? undefined : document._addEventListener(...args);
Object.defineProperties(document, {
hidden: { value: false },
visibilityState: { value: 'visible' },
hasFocus: { value: () => true },
onvisibilitychange: { get: () => undefined, set: () => { } },
onblur: { get: () => undefined, set: () => { } }
});
Object.defineProperties(win, {
onblur: { get: () => undefined, set: () => { } },
onpagehide: { get: () => undefined, set: () => { } }
});
}
// ---- 字体反混淆 ----
let glyphHashMap = null; // SHA-1哈希 -> 原始Unicode码点
let fontCharMap = null; // 混淆字符 -> 原始字符
let glyphHashMapPromise = null; // 映射表加载Promise
let deobfuscationPromise = null; // 反混淆进行中的Promise
// FontFace 构造函数拦截:捕获 new FontFace(family, ArrayBuffer) 形式注入的字体
// AI学习空间的混淆字体很可能从 JS 直接以 ArrayBuffer 注入而不走 @font-face / 网络请求
const capturedFontBuffers = []; // [{ family, buffer }]
function setupFontFaceHook() {
try {
const win = unsafeWindow;
const OriginalFontFace = win.FontFace;
if (!OriginalFontFace) { console.warn('[雨课堂助手] FontFace API 不可用'); return; }
const HookedFontFace = new Proxy(OriginalFontFace, {
construct(target, args) {
try {
const family = args[0];
const source = args[1];
if (source instanceof ArrayBuffer) {
capturedFontBuffers.push({ family, buffer: source });
console.log('[雨课堂助手] 🎯 拦截 FontFace(ArrayBuffer):', family, 'bytes:', source.byteLength);
} else if (source && ArrayBuffer.isView(source)) {
const sliced = source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
capturedFontBuffers.push({ family, buffer: sliced });
console.log('[雨课堂助手] 🎯 拦截 FontFace(TypedArray):', family, 'bytes:', source.byteLength);
} else if (typeof source === 'string') {
const m = source.match(/url\(["\']?([^"\')]+)["\']?\)/);
if (m) console.log('[雨课堂助手] 🎯 拦截 FontFace(URL):', family, m[1]);
}
} catch (e) { console.warn('[雨课堂助手] FontFace hook 异常:', e); }
return Reflect.construct(target, args, target);
}
});
win.FontFace = HookedFontFace;
} catch (e) {
console.warn('[雨课堂助手] setupFontFaceHook 失败:', e);
}
}
function loadGlyphHashMap() {
glyphHashMapPromise = new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://qsy.iano.cn/original_glyph_to_uni_v2.json',
onload: (res) => {
try {
glyphHashMap = JSON.parse(res.responseText);
console.log('[雨课堂助手] 字形映射表加载成功, 共', Object.keys(glyphHashMap).length, '个字形');
resolve(glyphHashMap);
} catch (e) {
console.error('[雨课堂助手] 解析字形映射表失败:', e);
reject(e);
}
},
onerror: (err) => {
console.error('[雨课堂助手] 加载字形映射表失败:', err);
reject(err);
}
});
});
return glyphHashMapPromise;
}
async function sha1Hex(str) {
const data = new TextEncoder().encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function buildFontCharMapFromBuffer(buffer, label) {
if (!glyphHashMap) {
console.warn('[雨课堂助手] 字形映射表未加载,跳过反混淆');
return null;
}
try {
const font = opentype.parse(buffer);
const charMap = {};
let matchCount = 0;
let glyphsUsable = 0;
for (let i = 0; i < font.glyphs.length; i++) {
const glyph = font.glyphs.get(i);
if (!glyph.unicode || !glyph.path || !glyph.path.commands || glyph.path.commands.length === 0) continue;
glyphsUsable++;
const pathStr = glyph.path.toPathData();
if (!pathStr) continue;
const hash = await sha1Hex(pathStr);
const originalUnicode = glyphHashMap[hash];
if (originalUnicode !== undefined && originalUnicode !== glyph.unicode) {
charMap[String.fromCodePoint(glyph.unicode)] = String.fromCodePoint(originalUnicode);
matchCount++;
}
}
console.log('[雨课堂助手] 字体反混淆映射建立完成:', matchCount, '个字符需要还原 (总字形:', font.glyphs.length, '有unicode+path:', glyphsUsable, label || '', ')');
if (matchCount > 0) fontCharMap = charMap;
return charMap;
} catch (e) {
console.error('[雨课堂助手] 解析字体失败:', label, e);
return null;
}
}
async function buildFontCharMap(fontUrl) {
if (!glyphHashMap) {
console.warn('[雨课堂助手] 字形映射表未加载,跳过反混淆');
return {};
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: fontUrl,
responseType: 'arraybuffer',
onload: async (res) => {
try {
const charMap = await buildFontCharMapFromBuffer(res.response, 'URL: ' + fontUrl);
if (charMap === null) { reject(new Error('font parse failed')); return; }
resolve(charMap);
} catch (e) {
console.error('[雨课堂助手] 解析混淆字体失败:', e);
reject(e);
}
},
onerror: (err) => {
console.error('[雨课堂助手] 下载混淆字体失败:', err);
reject(err);
}
});
});
}
// 对整段文本逐字替换(仅在拿不到 HTML 上下文时使用,例如已渲染后的 DOM innerText)
function deobfuscateText(text) {
if (!fontCharMap || !text) return text;
return Array.from(text).map(ch => fontCharMap[ch] || ch).join('');
}
// 从页面 CSS 中发现可能的混淆字体 URL(AI学习空间等场景 JSON Hook 拿不到 fontUrl 时使用)
function discoverEncryptedFontUrls() {
const candidates = new Set();
const isLikelyEncrypted = (text) => /encrypt(ed)?|xuetangx|yuketang|exam[_-]?data[_-]?decrypt|exam[_-]?font/i.test(text || '');
// 优先扫描 styleSheets(拿到的是已解析后的 CSSRule,比较干净)
for (const sheet of Array.from(document.styleSheets || [])) {
let rules;
try { rules = sheet.cssRules || sheet.rules; } catch (_) { continue; /* 跨域样式表无法读取 */ }
if (!rules) continue;
for (const rule of Array.from(rules)) {
// CSSRule.FONT_FACE_RULE === 5
if (rule.type !== 5) continue;
const family = (rule.style?.getPropertyValue?.('font-family') || '').replace(/['"]/g, '');
const src = rule.style?.getPropertyValue?.('src') || '';
if (!isLikelyEncrypted(family) && !isLikelyEncrypted(src)) continue;
const re = /url\(["']?([^"')]+\.(?:woff2?|ttf|otf)[^"')]*)["']?\)/gi;
let m;
while ((m = re.exec(src)) !== null) candidates.add(m[1]);
}
}
// 兜底:扫