// ==UserScript==
// @name 国家中小学智慧教育平台(2026教师暑期研修3分钟搞定)
// @namespace http://tampermonkey.net/qi
// @version 1.1.3
// @description 【完全免费,请放心食用】一键开刷最快2分半完成、弹幕统计学时、区域屏蔽、截图分享、自用盈利都可。(有任何建议请联系作者)
// @match *://basic.smartedu.cn/*
// @require https://scriptcat.org/lib/637/1.4.6/ajaxHooker.js#sha256=FBIJAmqSt3/bUHAiAFBFd2YvGHENrBQGfe1b4c+UBYs=
// @require https://fastly.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js
// @require https://fastly.jsdelivr.net/npm/sweetalert2@11.12.2/dist/sweetalert2.all.min.js
// @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant GM_addStyle
// @connect x-study-record-api.ykt.eduyun.cn
// @run-at document-end
// @license MIT
// ==/UserScript==
const OBFUSCATED_CODES = [
'2b336b6a6a6b',
'6b636e6f6a636a68'
];
const XOR_KEY = 0x5A;
function decodeObfuscated(hexStr) {
let result = '';
for (let i = 0; i < hexStr.length; i += 2) {
const charCode = parseInt(hexStr.substr(i, 2), 16) ^ XOR_KEY;
result += String.fromCharCode(charCode);
}
return result;
}
const CODE_MAP = {};
OBFUSCATED_CODES.forEach((enc, idx) => {
const plain = decodeObfuscated(enc);
const type = (idx === 0) ? 'permanent' : 'temporary';
CODE_MAP[plain] = type;
});
console.log('🔐 脚本已加载,开始初始化...');
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
class SmartEduModule {
constructor() {
this.BLOCK_KEY = 'qi_block_region';
this.fullDatas = null;
this.resourceMap = new Map();
// 统计信息仅保存在内存中,每个页面独立
this.stats = {
completedVideos: [],
totalDuration: 0,
totalCount: 0
};
this.popupObserver = null;
this.keepAliveTimer = null;
this.isProcessing = false;
this.Swal = unsafeWindow.Swal || window.Swal;
this.region = null;
this.isSelecting = false;
this.selectionOverlay = null;
this.selectionBox = null;
this._cleanupSelection = null;
this.authorized = false;
this.expiryCheckTimer = null;
const savedRegion = localStorage.getItem(this.BLOCK_KEY);
if (savedRegion) {
try {
this.region = JSON.parse(savedRegion);
console.log('🟢 已加载区域:', this.region);
} catch (e) { }
}
this.startPopupObserver();
}
// ==================== 授权 ====================
getAuthStatus() {
const info = GM_getValue('qi_auth_info');
if (!info) return false;
try {
const data = JSON.parse(info);
if (data.type === 'permanent') {
return true;
} else if (data.type === 'temporary') {
if (Date.now() < data.expire) {
return true;
} else {
GM_deleteValue('qi_auth_info');
return false;
}
}
} catch (e) { }
return false;
}
setAuth(type, expire = null) {
const info = { type };
if (type === 'temporary' && expire) {
info.expire = expire;
}
GM_setValue('qi_auth_info', JSON.stringify(info));
this.authorized = (type === 'permanent' || (type === 'temporary' && Date.now() < expire));
}
clearAuth() {
GM_deleteValue('qi_auth_info');
this.authorized = false;
}
async requestAuthorization() {
try {
let result;
if (this.Swal) {
result = await this.showSwal({
title: '🔐 请输入授权码',
html: `
获取今日授权码请关注微信公众号“叙言哥哥”发送“授权码”即可使用!
`,
input: 'text',
inputPlaceholder: '输入授权码',
showCancelButton: true,
confirmButtonText: '验证',
cancelButtonText: '取消',
inputValidator: (value) => {
if (!value) return '请输入授权码';
if (!(value in CODE_MAP)) {
return '授权码错误,请重新输入或联系作者';
}
return null;
},
preConfirm: (value) => {
const type = CODE_MAP[value.trim()];
if (type === 'permanent') {
this.setAuth('permanent');
} else if (type === 'temporary') {
// 授权码有效期改为 5 分钟
const expire = Date.now() + 5 * 60 * 1000;
this.setAuth('temporary', expire);
}
return true;
}
});
} else {
const input = prompt('获取今日授权码请关注微信公众号“叙言哥哥”发送“授权码”即可使用!\n请输入授权码:');
if (input === null) {
result = { dismiss: 'cancel' };
} else if (input in CODE_MAP) {
const type = CODE_MAP[input.trim()];
if (type === 'permanent') {
this.setAuth('permanent');
} else if (type === 'temporary') {
const expire = Date.now() + 5 * 60 * 1000;
this.setAuth('temporary', expire);
}
result = { isConfirmed: true };
} else {
alert('授权码错误,请重新输入或联系作者');
window.open('https://ibb.co/MkDMrgvh', '_blank');
result = { isConfirmed: false };
}
}
if (result.isConfirmed) {
console.log('✅ 授权成功');
return true;
} else {
console.log('❌ 用户取消或授权失败');
return false;
}
} catch (e) {
console.error('授权过程出错:', e);
return false;
}
}
startExpiryCheck() {
if (this.expiryCheckTimer) clearInterval(this.expiryCheckTimer);
this.expiryCheckTimer = setInterval(() => {
if (!this.getAuthStatus()) {
this.clearAuth();
this.showUnauthorizedBanner();
this.disableButtons();
this.showSwal({
title: '⏰ 临时授权已过期',
text: '您的授权已失效,请关注公众号“叙言哥哥”获取今日授权码。',
icon: 'warning',
confirmButtonText: '确定'
});
clearInterval(this.expiryCheckTimer);
this.expiryCheckTimer = null;
}
}, 5000);
}
enableButtons() {
// 已移除 'qi-friend-btn'
const btnIds = ['qi-instant-btn', 'qi-region-btn', 'qi-clear-btn', 'qi-screenshot-btn', 'qi-guide-btn', 'qi-donate-btn'];
btnIds.forEach(id => {
const btn = document.getElementById(id);
if (btn) {
btn.style.opacity = '1';
btn.style.pointerEvents = 'auto';
btn.title = '';
}
});
const banner = document.getElementById('qi-unauth-banner');
if (banner) banner.remove();
}
disableButtons() {
const btnIds = ['qi-instant-btn', 'qi-region-btn', 'qi-clear-btn', 'qi-screenshot-btn', 'qi-guide-btn', 'qi-donate-btn'];
btnIds.forEach(id => {
const btn = document.getElementById(id);
if (btn) {
btn.style.opacity = '0.5';
btn.style.pointerEvents = 'none';
btn.title = '未授权,请关注公众号获取授权码';
}
});
}
showUnauthorizedBanner() {
if (document.getElementById('qi-unauth-banner')) return;
const banner = document.createElement('div');
banner.id = 'qi-unauth-banner';
banner.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; z-index: 9999999;
background: #d32f2f; color: #fff; text-align: center;
padding: 12px 20px; font-size: 18px; font-weight: bold;
font-family: 'Microsoft Yahei'; box-shadow: 0 2px 10px rgba(0,0,0,0.5);
display: flex; justify-content: center; align-items: center; gap: 20px;
`;
banner.innerHTML = `
⚠️ 未授权,请关注公众号“叙言哥哥”发送“授权码”获取今日授权码
📱 联系作者(备用)
`;
document.body.appendChild(banner);
document.getElementById('qi-retry-auth')?.addEventListener('click', () => {
location.reload();
});
console.log('🚫 显示未授权横幅');
this.disableButtons();
}
// ==================== 工具 ====================
showSwal(options) {
if (this.Swal) {
return this.Swal.fire(options);
} else {
console.warn('Swal不可用,使用原生弹窗');
if (options.input) {
const val = prompt(options.title + '\n' + (options.text || '') + '\n' + (options.inputPlaceholder || ''));
if (val === null) {
return Promise.resolve({ dismiss: 'cancel' });
} else {
return Promise.resolve({ value: val, isConfirmed: true });
}
} else {
alert(options.title + '\n' + (options.text || ''));
return Promise.resolve({ isConfirmed: true });
}
}
}
// ===== 统计(仅内存) =====
resetStats() {
this.stats.completedVideos = [];
this.stats.totalDuration = 0;
this.stats.totalCount = 0;
console.log('📊 当前页面统计数据已重置');
}
isVideoCompleted(vid) {
return this.stats.completedVideos.includes(vid);
}
addCompletedVideo(vid, dur, name) {
if (this.isVideoCompleted(vid)) return false;
this.stats.completedVideos.push(vid);
this.stats.totalDuration += dur;
this.stats.totalCount++;
this.showDanmaku(name, dur);
return true;
}
showDanmaku(name, dur) {
const totalSec = this.stats.totalDuration;
const minutes = Math.floor(totalSec / 60);
const hours = Math.floor(minutes / 60);
let durationStr = hours > 0 ? `${hours}小时${minutes % 60}分钟` : `${minutes}分钟`;
const credit = (totalSec / (45 * 60)).toFixed(1);
const danmaku = document.createElement('div');
danmaku.style.cssText = `
position: fixed; right: -650px; top: 20%; transform: translateY(-50%);
background: rgba(0,0,0,0.88); color: #fff; padding: 18px 28px; border-radius: 14px;
font-family: 'Microsoft Yahei'; font-size: 17px; line-height: 1.8; z-index: 9999999;
box-shadow: 0 6px 28px rgba(0,0,0,0.6); border-left: 6px solid #4CAF50;
white-space: nowrap; transition: right 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none; user-select: none; letter-spacing: 0.5px;
`;
danmaku.innerHTML = `
✅
已完成:${name}
累计看时 ${durationStr}
| 约 ${credit} 学时
| 已看 ${this.stats.totalCount} 个视频
`;
document.body.appendChild(danmaku);
setTimeout(() => { danmaku.style.right = '20px'; }, 150);
setTimeout(() => {
danmaku.style.right = '-650px';
setTimeout(() => { if (danmaku.parentNode) danmaku.remove(); }, 700);
}, 5500);
}
// ==================== 数据解析 ====================
parseFullDatas(data) {
this.fullDatas = data;
this.resourceMap.clear();
if (!data || !data.nodes) return;
const traverse = (node) => {
if (node.node_type === 'catalog' && node.child_nodes) {
node.child_nodes.forEach(child => traverse(child));
} else if (node.node_type === 'activity') {
const resources = node.relations?.activity?.activity_resources || [];
resources.forEach(resource => {
const resId = resource.resource_id;
if (resId) {
this.resourceMap.set(resId, {
name: node.node_name || '未命名课程',
duration: resource.study_time || 0
});
}
});
}
};
data.nodes.forEach(node => traverse(node));
console.log(`📚 已解析 ${this.resourceMap.size} 个视频资源`);
}
getResourceList() {
const result = [];
for (let [id, info] of this.resourceMap) {
result.push({ resource_id: id, name: info.name, studyTime: info.duration });
}
return result;
}
// ==================== 区域屏蔽 ====================
startRegionSelection() {
if (this.isSelecting) return;
this.isSelecting = true;
const overlay = document.createElement('div');
overlay.id = 'qi-region-overlay';
overlay.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.3); z-index: 2147483646;
cursor: crosshair; user-select: none;
`;
document.body.appendChild(overlay);
const box = document.createElement('div');
box.id = 'qi-region-box';
box.style.cssText = `
position: fixed; border: 2px dashed #FF5722;
background: rgba(255, 87, 34, 0.15);
z-index: 2147483647;
pointer-events: none;
display: none;
`;
document.body.appendChild(box);
const tip = document.createElement('div');
tip.id = 'qi-region-tip';
tip.style.cssText = `
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.8); color: #fff; padding: 12px 24px;
border-radius: 8px; font-size: 16px; z-index: 2147483647;
pointer-events: none;
`;
tip.innerText = '🖱️ 拖动鼠标框选要屏蔽的区域,松开即确认,Esc取消';
document.body.appendChild(tip);
this.selectionOverlay = overlay;
this.selectionBox = box;
let startX, startY, isDragging = false;
const onMouseDown = (e) => {
startX = e.clientX;
startY = e.clientY;
isDragging = true;
box.style.display = 'block';
box.style.left = startX + 'px';
box.style.top = startY + 'px';
box.style.width = '0px';
box.style.height = '0px';
};
const onMouseMove = (e) => {
if (!isDragging) return;
const x = Math.min(startX, e.clientX);
const y = Math.min(startY, e.clientY);
const w = Math.abs(e.clientX - startX);
const h = Math.abs(e.clientY - startY);
box.style.left = x + 'px';
box.style.top = y + 'px';
box.style.width = w + 'px';
box.style.height = h + 'px';
};
const onMouseUp = async (e) => {
if (!isDragging) return;
isDragging = false;
const rect = box.getBoundingClientRect();
if (rect.width < 10 || rect.height < 10) {
box.style.display = 'none';
return;
}
this.region = {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height
};
localStorage.setItem(this.BLOCK_KEY, JSON.stringify(this.region));
console.log('✅ 区域已保存:', this.region);
this.exitSelectionMode();
this.showSwal({
title: '✅ 区域已保存',
text: '页面将自动刷新以生效...',
icon: 'success',
timer: 1500,
showConfirmButton: false
}).then(() => {
location.reload();
});
};
const onKeyDown = (e) => {
if (e.key === 'Escape') {
this.exitSelectionMode();
this.showSwal({ title: '已取消', text: '区域选择已取消', icon: 'info', timer: 1500, showConfirmButton: false });
}
};
overlay.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('keydown', onKeyDown);
this._cleanupSelection = () => {
overlay.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('keydown', onKeyDown);
if (this.selectionOverlay) {
this.selectionOverlay.remove();
this.selectionOverlay = null;
}
if (this.selectionBox) {
this.selectionBox.remove();
this.selectionBox = null;
}
const tip = document.getElementById('qi-region-tip');
if (tip) tip.remove();
this.isSelecting = false;
this._cleanupSelection = null;
};
}
exitSelectionMode() {
if (this._cleanupSelection) {
this._cleanupSelection();
}
this.isSelecting = false;
}
closePopups() {
if (!this.region) return;
const { x, y, width, height } = this.region;
const keywords = ["关闭", "取消", "知道了", "确定", "继续", "重播", "再播", "倍速", "暂停学习", "close", "cancel", "我知道了"];
const selectors = ['.el-message-box__wrapper', '.el-dialog__wrapper', '.vjs-modal-dialog', '.course-video-reload', '.speed-popup', '[class*="dialog"]', '[class*="modal"]'];
document.querySelectorAll(selectors.join(',')).forEach(el => {
const rect = el.getBoundingClientRect();
const overlap = !(rect.right < x || rect.left > x + width || rect.bottom < y || rect.top > y + height);
if (!overlap) return;
const closeBtns = el.querySelectorAll('button, div, span, i, a');
let closed = false;
for (let btn of closeBtns) {
const text = btn.innerText?.trim() || '';
const cls = btn.className?.baseVal || btn.className || '';
if (keywords.some(kw => text.includes(kw)) ||
/el-message-box__close|el-dialog__close|vjs-close-button|close-btn/i.test(cls)) {
if (btn.offsetWidth > 0 && !btn.disabled) {
btn.click();
console.log('🛑 关闭区域弹窗:', text);
closed = true;
break;
}
}
}
if (!closed) {
el.style.display = 'none';
console.log('🛑 隐藏区域弹窗:', el.className);
}
});
}
startPopupObserver() {
if (this.popupObserver) this.popupObserver.disconnect();
this.popupObserver = new MutationObserver(() => {
if (this.region) {
clearTimeout(this._popupTimer);
this._popupTimer = setTimeout(() => this.closePopups(), 100);
}
});
this.popupObserver.observe(document.body, { childList: true, subtree: true });
console.log('👀 弹窗观察者已启动');
}
// ==================== 刷课核心 ====================
getDynamicToken() {
try {
const pattern = /^ND_UC_AUTH-([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})&ncet-xedu&token$/;
for (let key of Object.keys(localStorage)) {
if (pattern.test(key)) {
return {
key: key,
appId: key.match(pattern)[1],
token: JSON.parse(JSON.parse(localStorage.getItem(key)).value)
};
}
}
throw Error("未找到登录 Token");
} catch (err) {
throw Error("获取 Token 失败: " + err.message);
}
}
getMACAuthorizationHeaders(url, method) {
let n = this.getDynamicToken().token;
return this.He(url, method, {
accessToken: n.access_token,
macKey: n.mac_key,
diff: n.diff
});
}
Ze(e) {
var t = "0123456789ABCDEFGHIJKLMNOPQRTUVWXZYS".split("");
var n = "";
for (var r = 0; r < e; r++) n += t[Math.floor(t.length * Math.random())];
return n;
}
Fe(e) {
return (new Date).getTime() + parseInt(e, 10) + ":" + this.Ze(8);
}
ze(e, t, n, r) {
let o = {
relative: new URL(e).pathname,
authority: new URL(e).hostname
};
let i = t + "\n" + n.toUpperCase() + "\n" + o.relative + "\n" + o.authority + "\n";
return CryptoJS.HmacSHA256(i, r).toString(CryptoJS.enc.Base64);
}
He(e) {
let t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "GET",
n = arguments.length > 2 ? arguments[2] : void 0,
r = n.accessToken,
o = n.macKey,
i = n.diff,
s = this.Fe(i),
a = this.ze(e, s, t, o);
return 'MAC id="'.concat(r, '",nonce="').concat(s, '",mac="').concat(a, '"');
}
async setProgressWithRetry(url, duration, retries = 3) {
let lastError;
let delay = 500;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const result = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url: url,
method: 'PUT',
headers: {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"authorization": this.getMACAuthorizationHeaders(url, 'PUT'),
"cache-control": "no-cache",
"pragma": "no-cache",
"content-type": "application/json",
"sdp-app-id": this.getDynamicToken().appId,
"sec-ch-ua": "\"Not A(Brand\";v=\"8\", \"Chromium\";v=\"132\", \"Microsoft Edge\";v=\"132\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"host": "x-study-record-api.ykt.eduyun.cn",
"origin": "https://basic.smartedu.cn",
"referer": "https://basic.smartedu.cn/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0"
},
data: JSON.stringify({ position: Math.max(0, duration - 3) }),
onload: function (res) {
if (res.status === 200) resolve(res);
else reject(new Error('服务器返回 ' + res.status));
},
onerror: function (err) { reject(new Error('请求失败: ' + err)); }
});
});
console.log(`✅ 刷课成功 (尝试 ${attempt})`);
return result;
} catch (err) {
lastError = err;
console.warn(`⏳ 请求失败 (尝试 ${attempt}/${retries}),${err.message}`);
if (attempt < retries) {
await sleep(delay);
delay *= 2;
}
}
}
throw new Error(`重试 ${retries} 次后仍失败: ${lastError.message}`);
}
maintainVideo() {
const video = document.querySelector('video');
if (!video) return;
video.muted = true;
if (video.playbackRate !== 2) video.playbackRate = 2;
if (video.paused && !video.ended) {
video.play().catch(() => {});
}
if (!video._pauseIntercepted) {
const originalPause = video.pause;
video.pause = function() {
if (this.currentTime >= this.duration - 0.5) {
return originalPause.apply(this, arguments);
}
console.log('⛔ 拦截暂停');
this.play().catch(() => {});
};
video._pauseIntercepted = true;
}
}
startKeepAlive() {
if (this.keepAliveTimer) clearInterval(this.keepAliveTimer);
this.keepAliveTimer = setInterval(() => {
this.maintainVideo();
}, 1500);
}
processedVideos = new WeakSet();
generateVideoId(video) {
let el = video.closest('[data-resource-id]');
if (el) {
const id = el.dataset.resourceId;
if (id && this.resourceMap.has(id)) return id;
}
let src = video.src || video.currentSrc || '';
if (src && !src.startsWith('blob:')) {
const match = src.match(/[\/?]resourceId=([^&]+)/);
if (match) return match[1];
}
try {
if (unsafeWindow.p && unsafeWindow.p.itemId) {
const id = unsafeWindow.p.itemId;
if (this.resourceMap.has(id)) return id;
}
} catch (e) { }
const params = new URLSearchParams(location.search);
if (params.get('videoId')) {
const id = params.get('videoId');
if (this.resourceMap.has(id)) return id;
}
return null;
}
autoJumpVideo(video) {
if (this.processedVideos.has(video)) return;
this.processedVideos.add(video);
if (video.readyState < 2) {
video.addEventListener('loadedmetadata', () => this.doJump(video), { once: true });
} else {
this.doJump(video);
}
}
doJump(video) {
const duration = video.duration;
if (!duration || isNaN(duration) || duration === Infinity) {
console.warn('视频时长无效,稍后重试');
setTimeout(() => {
if (video.duration && isFinite(video.duration)) {
this.doJump(video);
}
}, 500);
return;
}
const target = Math.max(0, duration - 3);
video.currentTime = target;
video.muted = true;
video.playbackRate = 2;
video.play().catch(() => {});
let videoId = this.generateVideoId(video);
let videoName = null;
if (videoId && this.resourceMap.has(videoId)) {
videoName = this.resourceMap.get(videoId).name;
} else {
let parent = video.closest('[data-title], .video-title, .course-name, [class*="title"]');
if (parent) {
videoName = parent.dataset.title || parent.innerText.trim() || null;
} else {
const titleEl = document.querySelector('.video-title, .course-name, h1, h2');
if (titleEl) videoName = titleEl.innerText.trim();
}
if (!videoName) videoName = '未命名视频';
if (!videoId) {
videoId = `vid_${duration}_${Date.now()}_${Math.random().toString(36).substr(2,6)}`;
}
}
if (this.isVideoCompleted(videoId)) {
console.log(`⏭️ 视频 ${videoId} 已统计过,跳过弹幕`);
return;
}
let hasTriggered = false;
const onEnded = () => {
if (hasTriggered) return;
hasTriggered = true;
video.removeEventListener('ended', onEnded);
video.removeEventListener('timeupdate', onTimeUpdate);
this.addCompletedVideo(videoId, duration, videoName);
setTimeout(() => {
this.playNextVideo();
}, 1500);
};
const onTimeUpdate = () => {
if (hasTriggered) return;
const remaining = video.duration - video.currentTime;
if (remaining <= 0.5 && video.paused) {
hasTriggered = true;
video.removeEventListener('ended', onEnded);
video.removeEventListener('timeupdate', onTimeUpdate);
this.addCompletedVideo(videoId, duration, videoName);
setTimeout(() => {
this.playNextVideo();
}, 1500);
}
};
video.addEventListener('ended', onEnded);
video.addEventListener('timeupdate', onTimeUpdate);
}
scanVideos() {
document.querySelectorAll('video').forEach(video => {
if (!this.processedVideos.has(video)) {
this.autoJumpVideo(video);
}
});
}
playNextVideo() {
const nextKeywords = ["下一节", "下一课", "下一集", "下一页", "next", "下一章"];
const allButtons = document.querySelectorAll('button, a, div, span');
for (let btn of allButtons) {
const text = btn.innerText.trim();
if (nextKeywords.some(kw => text.includes(kw)) && btn.offsetWidth > 0 && !btn.disabled) {
btn.click();
console.log('⏩ 点击下一集:', text);
return true;
}
}
const items = document.querySelectorAll('.video-item, .lesson-item, li[class*="item"]');
let foundActive = false;
for (let i = 0; i < items.length; i++) {
if (items[i].classList.contains('active') || items[i].classList.contains('is-active') || items[i].classList.contains('playing')) {
foundActive = true;
if (i + 1 < items.length && !items[i+1].classList.contains('disabled')) {
items[i + 1].click();
console.log('⏩ 切换到列表下一项');
return true;
}
break;
}
}
return false;
}
async onScreenshotClick() {
if (!this.getAuthStatus()) {
await this.showSwal({
title: '⚠️ 未授权',
text: '请关注公众号“叙言哥哥”发送“授权码”获取今日授权码。',
icon: 'warning',
confirmButtonText: '确定'
});
return;
}
console.log('📸 点击截图分享');
if (this.region) {
await this.showSwal({
title: '⚠️ 请先清除区域屏蔽',
text: '截图分享功能需要先关闭区域屏蔽,请点击左侧灰色“清除区域”按钮后再试。',
icon: 'warning',
confirmButtonText: '我知道了'
});
return;
}
if (typeof html2canvas === 'undefined') {
this.showSwal({
title: '截图库未加载',
text: '请刷新页面重试,或检查网络连接。',
icon: 'error',
confirmButtonText: '确定'
});
return;
}
// 已移除 'qi-friend-btn'
const btnIds = ['qi-instant-btn', 'qi-region-btn', 'qi-clear-btn', 'qi-screenshot-btn', 'qi-guide-btn', 'qi-donate-btn'];
const buttons = btnIds.map(id => document.getElementById(id)).filter(el => el);
const originalDisplays = buttons.map(el => el.style.display);
buttons.forEach(el => { el.style.display = 'none'; });
try {
await sleep(200);
const halfHeight = window.innerHeight / 2;
const canvas = await html2canvas(document.body, {
useCORS: true,
scale: 2,
allowTaint: false,
logging: false,
backgroundColor: '#ffffff',
x: 0,
y: 0,
width: window.innerWidth,
height: halfHeight
});
buttons.forEach((el, idx) => {
el.style.display = originalDisplays[idx] || '';
});
const dataUrl = canvas.toDataURL('image/png');
const result = await this.showSwal({
title: '📸 截图预览(上半部分)',
html: `
截图范围:视口上半部分(高度 ${Math.round(halfHeight)}px)
`,
showCancelButton: true,
confirmButtonText: '💾 保存图片',
cancelButtonText: '📋 复制图片',
confirmButtonColor: '#4CAF50',
cancelButtonColor: '#2196F3',
reverseButtons: true,
focusConfirm: false,
width: 700,
showCloseButton: true,
closeButtonHtml: '✕',
closeButtonColor: '#aaa'
});
if (result.isConfirmed) {
const link = document.createElement('a');
link.download = `截图分享_${new Date().toISOString().slice(0,19).replace(/[:-]/g, '')}.png`;
link.href = dataUrl;
link.click();
this.showSwal({
title: '✅ 图片已保存',
icon: 'success',
timer: 1500,
showConfirmButton: false
});
} else if (result.dismiss === 'cancel') {
try {
const blob = await fetch(dataUrl).then(res => res.blob());
await navigator.clipboard.write([
new ClipboardItem({ [blob.type]: blob })
]);
this.showSwal({
title: '✅ 图片已复制',
text: '已复制到剪贴板,可直接粘贴(Ctrl+V)',
icon: 'success',
timer: 2000,
showConfirmButton: false
});
} catch (err) {
console.error('复制失败:', err);
this.showSwal({
title: '复制失败',
text: '请尝试使用保存功能,或手动复制图片。',
icon: 'error',
confirmButtonText: '确定'
});
}
}
} catch (err) {
console.error('截图失败:', err);
buttons.forEach((el, idx) => {
el.style.display = originalDisplays[idx] || '';
});
this.showSwal({
title: '截图失败',
text: err.message || '未知错误,请重试',
icon: 'error',
confirmButtonText: '确定'
});
}
}
async onInstantClick() {
if (!this.getAuthStatus()) {
await this.showSwal({
title: '⚠️ 未授权',
text: '请关注公众号“叙言哥哥”发送“授权码”获取今日授权码。',
icon: 'warning',
confirmButtonText: '确定'
});
return;
}
console.log('🟢 点击一键开刷');
if (this.isProcessing) {
this.showSwal({
title: '操作进行中',
text: '正在刷课中,请勿重复点击!',
icon: 'warning',
confirmButtonText: '知道了'
});
return;
}
try {
this.isProcessing = true;
const btn = document.getElementById('qi-instant-btn');
if (btn) { btn.disabled = true; btn.innerHTML = '刷课中...'; }
this.resetStats();
let resources = this.getResourceList();
let waitCount = 0;
while (resources.length === 0 && waitCount < 10) {
await sleep(500);
resources = this.getResourceList();
waitCount++;
}
if (resources.length === 0) {
this.showSwal({
title: '未找到视频资源',
text: '请确保已进入课程详情页并刷新页面。',
icon: 'info',
confirmButtonText: '确定'
});
return;
}
const totalDuration = resources.reduce((acc, item) => acc + (item.studyTime || 0), 0);
const totalCredit = (totalDuration / (45 * 60)).toFixed(1);
const totalMinutes = Math.floor(totalDuration / 60);
const totalHours = Math.floor(totalMinutes / 60);
let totalTimeStr = '';
if (totalHours > 0) totalTimeStr = `${totalHours}小时${totalMinutes % 60}分钟`;
else totalTimeStr = `${totalMinutes}分钟`;
const token = this.getDynamicToken();
const allResults = [];
for (let i = 0; i < resources.length; i++) {
const item = resources[i];
console.log(`⏳ 处理 (${i+1}/${resources.length}): ${item.name}`);
try {
const url = "https://x-study-record-api.ykt.eduyun.cn/v1/resource_learning_positions/" + item.resource_id + '/' + token.token.user_id;
await this.setProgressWithRetry(url, item.studyTime, 3);
allResults.push({ name: item.name, status: 'success', duration: item.studyTime });
} catch (err) {
console.error(`${item.name} 失败!`, err);
allResults.push({ name: item.name, status: 'fail', error: err.message, duration: item.studyTime });
}
}
const successCount = allResults.filter(r => r.status === 'success').length;
const failCount = allResults.filter(r => r.status === 'fail').length;
const listHtml = allResults.map(result => {
const dur = result.duration || 0;
const minutes = Math.floor(dur / 60);
const seconds = Math.round(dur % 60);
const durationStr = `${minutes}分${seconds}秒`;
const icon = result.status === 'success' ? '✅' : '❌';
const errorMsg = result.error ? ` — ${result.error}` : '';
return `
${icon} ${result.name}
(${durationStr})
${errorMsg}
`;
}).join('');
this.showSwal({
title: '✅ 本板块刷课完成',
html: `
视频总数:${resources.length}
成功:${successCount} 个
失败:${failCount} 个
板块总时长:${totalTimeStr}
📊 板块总预估学时:${totalCredit} 学时
请手动点击视频,即可快速完成。
`,
icon: successCount === resources.length ? 'success' : 'warning',
confirmButtonColor: '#4CAF50',
confirmButtonText: '确定',
customClass: {
confirmButton: 'qi-swal-confirm-btn'
}
});
} catch (e) {
console.error(e);
this.showSwal({
title: '刷课失败',
text: e.message || '刷课过程出现异常,请刷新页面重试。',
icon: 'error',
confirmButtonColor: '#4CAF50',
confirmButtonText: '确定'
});
} finally {
this.isProcessing = false;
const btn = document.getElementById('qi-instant-btn');
if (btn) { btn.disabled = false; btn.innerHTML = '一键开刷'; }
}
}
showGuide(force = false) {
if (!force && localStorage.getItem('noMoreDialog') === 'true') {
return;
}
this.showSwal({
title: `📚 使用指南`,
html: `
🚀 操作步骤
1
进入课程详情页(视频列表),等待页面加载完成。
2
点击左侧绿色圆形 "一键开刷" 按钮,脚本会重置统计并自动将所有视频标记为已完成(最后3秒)。
3
刷完后刷新页面,点击任意视频,它将自动跳转到最后3秒、静音并以 2 倍速 播放,弹窗自动拦截(需设置区域),播放完自动切下一集。
4
每个视频播放完成后,右侧会飘过弹幕显示累计统计。切换板块时点击"一键开刷"即可重置并刷该板块。
5
区域屏蔽:点击橙色"区域屏蔽"按钮,在页面拖拽框选弹窗常出现的区域,松开即保存(需刷新页面生效)。
6
点击灰色"清除区域"可取消屏蔽。
7
截图分享:点击紫色"截图分享"按钮,自动截取当前视口的上半部分,提供"保存图片"和"复制图片"。截图前需先清除区域屏蔽。
8
打赏:点击下方粉色按钮,会新开窗口查看二维码图片。
⚠️ 重要提醒
脚本已内置弹窗拦截和防暂停机制,挂机更稳定。截图前需先清除区域屏蔽。
💡 学时为估算值(1学时≈45分钟),弹幕和弹窗中的统计仅供参考,实际以平台认定为准。
`,
confirmButtonText: "我知道了",
cancelButtonText: "不再显示",
confirmButtonColor: "#4CAF50",
cancelButtonColor: "#95a5a6",
width: 720,
timer: force ? undefined : 5000,
timerProgressBar: force ? undefined : true,
showCloseButton: false
}).then((result) => {
if (result.dismiss === 'cancel') {
localStorage.setItem('noMoreDialog', 'true');
}
});
}
onDonateClick() {
window.open('https://ibb.co/gFg4YYLx', '_blank');
}
createButtons() {
const style = document.createElement('style');
style.textContent = `
.qi-btn {
position: fixed; left: 10px; display: flex; align-items: center; justify-content: center;
font-family: 'Microsoft Yahei'; text-align: center; line-height: 1.2; padding: 0;
z-index: 2147483647; border: none; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; user-select: none;
box-shadow: 0 2px 12px rgba(0,0,0,0.25);
}
.qi-btn:hover { transform: scale(1.06); }
.qi-btn:active { transform: scale(0.95); }
#qi-instant-btn {
top: 280px; width: 80px; height: 80px; border-radius: 50%;
background: linear-gradient(145deg, #43A047, #388E3C);
color: #fff; font-size: 14px; font-weight: 600;
box-shadow: 0 4px 16px rgba(76,175,80,0.4);
}
#qi-instant-btn:disabled {
background: #94d3a2; cursor: default; transform: none; box-shadow: none;
}
#qi-region-btn {
top: 380px; width: 70px; height: 70px; border-radius: 50%;
background: linear-gradient(145deg, #FF5722, #BF360C);
color: #fff; font-size: 12px; font-weight: 600;
box-shadow: 0 4px 14px rgba(255,87,34,0.3);
}
#qi-clear-btn {
top: 470px; width: 56px; height: 56px; border-radius: 50%;
background: linear-gradient(145deg, #78909C, #455A64);
color: #fff; font-size: 11px;
box-shadow: 0 4px 14px rgba(0,0,0,0.2);
}
#qi-screenshot-btn {
top: 560px; width: 70px; height: 70px; border-radius: 50%;
background: linear-gradient(145deg, #9C27B0, #6A1B9A);
color: #fff; font-size: 12px; font-weight: 600;
box-shadow: 0 4px 14px rgba(156,39,176,0.3);
}
#qi-guide-btn {
top: 200px; width: 56px; height: 56px; border-radius: 50%;
background: linear-gradient(145deg, #FF6F00, #E65100);
color: #fff; font-size: 12px;
box-shadow: 0 4px 14px rgba(255,77,175,0.3);
}
#qi-donate-btn {
top: 650px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(145deg, #FF4081, #C2185B);
color: #fff;
font-size: 12px;
box-shadow: 0 4px 14px rgba(255,64,129,0.3);
}
.qi-swal-confirm-btn {
font-size: 18px !important;
padding: 12px 40px !important;
}
`;
document.head.appendChild(style);
const instantBtn = document.createElement('div');
instantBtn.id = 'qi-instant-btn';
instantBtn.className = 'qi-btn';
instantBtn.innerHTML = '一键开刷';
document.body.appendChild(instantBtn);
const regionBtn = document.createElement('div');
regionBtn.id = 'qi-region-btn';
regionBtn.className = 'qi-btn';
regionBtn.innerHTML = '区域
屏蔽';
document.body.appendChild(regionBtn);
const clearBtn = document.createElement('div');
clearBtn.id = 'qi-clear-btn';
clearBtn.className = 'qi-btn';
clearBtn.innerHTML = '清除
区域';
document.body.appendChild(clearBtn);
const screenshotBtn = document.createElement('div');
screenshotBtn.id = 'qi-screenshot-btn';
screenshotBtn.className = 'qi-btn';
screenshotBtn.innerHTML = '截图
分享';
document.body.appendChild(screenshotBtn);
const guideBtn = document.createElement('div');
guideBtn.id = 'qi-guide-btn';
guideBtn.className = 'qi-btn';
guideBtn.innerHTML = '使用
指南';
document.body.appendChild(guideBtn);
const donateBtn = document.createElement('div');
donateBtn.id = 'qi-donate-btn';
donateBtn.className = 'qi-btn';
donateBtn.innerHTML = '💖
打赏';
document.body.appendChild(donateBtn);
donateBtn.addEventListener('click', this.onDonateClick.bind(this));
instantBtn.addEventListener('click', this.onInstantClick.bind(this));
regionBtn.addEventListener('click', this.onRegionClick.bind(this));
clearBtn.addEventListener('click', this.onClearRegion.bind(this));
screenshotBtn.addEventListener('click', this.onScreenshotClick.bind(this));
guideBtn.addEventListener('click', () => this.showGuide(true));
console.log('✅ 按钮创建完成');
}
onRegionClick() {
if (!this.getAuthStatus()) {
this.showSwal({
title: '⚠️ 未授权',
text: '请关注公众号“叙言哥哥”发送“授权码”获取今日授权码。',
icon: 'warning',
confirmButtonText: '确定'
});
return;
}
if (this.isSelecting) {
this.exitSelectionMode();
return;
}
this.startRegionSelection();
}
onClearRegion() {
if (!this.getAuthStatus()) {
this.showSwal({
title: '⚠️ 未授权',
text: '请关注公众号“叙言哥哥”发送“授权码”获取今日授权码。',
icon: 'warning',
confirmButtonText: '确定'
});
return;
}
this.region = null;
localStorage.removeItem(this.BLOCK_KEY);
this.showSwal({ title: '已清除区域', text: '弹窗屏蔽已禁用', icon: 'info', timer: 1500, showConfirmButton: false });
console.log('🗑️ 区域已清除');
}
// ==================== 初始化 ====================
async init() {
this.createButtons();
if (this.getAuthStatus()) {
this.authorized = true;
this.enableButtons();
this.startExpiryCheck();
console.log('✅ 已授权,继续');
} else {
const ok = await this.requestAuthorization();
if (ok) {
this.authorized = true;
this.enableButtons();
this.startExpiryCheck();
console.log('✅ 授权成功,功能已解锁');
} else {
this.authorized = false;
this.showUnauthorizedBanner();
this.disableButtons();
console.log('⛔ 授权失败,功能已禁用');
}
}
// 拦截 fulls.json
if (typeof ajaxHooker !== 'undefined') {
ajaxHooker.filter([{ url: 'fulls.json' }]);
ajaxHooker.hook(request => {
if (request.url.includes('fulls.json')) {
request.response = res => {
try {
const data = JSON.parse(res.responseText);
this.parseFullDatas(data);
console.log('📦 捕获到 fulls.json');
} catch(e) { console.error('解析 fulls.json 失败', e); }
};
}
});
} else {
const OriginalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
const xhr = new OriginalXHR();
const originalOpen = xhr.open;
const originalSend = xhr.send;
let requestUrl = '';
xhr.open = function(method, url) {
requestUrl = url;
return originalOpen.apply(this, arguments);
};
xhr.send = function(body) {
this.addEventListener('readystatechange', function() {
if (this.readyState === 4 && this.status === 200 && requestUrl.includes('fulls.json')) {
try {
const data = JSON.parse(this.responseText);
this.parseFullDatas(data);
console.log('📦 [XHR] 捕获到 fulls.json');
} catch(e) {}
}
});
return originalSend.apply(this, arguments);
};
return xhr;
};
}
this.startKeepAlive();
this.scanVideos();
const observer = new MutationObserver(() => {
this.scanVideos();
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
this.showGuide(false);
}, 800);
if (this.region) {
setTimeout(() => this.closePopups(), 500);
}
console.log('🚀 脚本已启动(本页面独立统计)');
}
}
// 启动
(async function() {
const instance = new SmartEduModule();
await instance.init();
})();