// ==UserScript==
// @name 国家智慧中小学秒刷(2026暑期教师研修基础教育学习助手)
// @namespace https://basic.smartedu.cn/
// @version 1.0.1
// @description 2026暑期教师研修自动完成学习(秒刷),免费、高效安全不检测,一键开启自动刷课,3秒钟搞定。
// @match *://basic.smartedu.cn/*
// @match *://https://www.smartedu.cn/*
// @match *://https://www.smartedu.cn/jiaoshi/*
// @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
// @connect elearning-train-api.ykt.eduyun.cn
// @connect elearning-train-gateway.ykt.eduyun.cn
// @connect elearning-train-gateway-safe.ykt.eduyun.cn
// @connect *.ykt.cbern.com.cn
// @icon https://basic.smartedu.cn/favicon.ico
// @run-at document-idle
// @license GPL-3.0
// @tag 国家中小学智慧教育平台 暑期培训 教师研修 自动学习 学习辅助
// ==/UserScript==
(function () {
'use strict';
// ================================================
const DONATE_IMG = 'https://s3.bmp.ovh/2026/08/25/5GaQOztG.png';
const QR_IMG = 'https://s3.bmp.ovh/2026/08/26/Y8okz1Ci.jpg';
// ================================================
const CONFIG = {
SDP_APP_ID: 'e5649925-441d-4a53-b525-51a2f1c4e0a8',
TRAINING_ID: 'dc6d78f2-bad8-4d09-b8da-0d758803dbe4',
LIBRARY_REF: 'bb042e69-9a11-49a1-af22-0c3fab2e92b9',
CDN_POOL: ['s-file-1', 's-file-2', 'bdcs-file-1', 'bdcs-file-2'],
TRAIN_TAG_SET: '2026年\u201c暑期教师研修\u201d专题(基础教育)',
TRAIN_ORIGIN_LABEL: '2026年\u201c暑期教师研修\u201d专题',
ELECTIVE_TARGET_HOURS: 3,
ELECTIVE_COURSE_TITLE: '学校美育浸润行动',
RETRY_DELAY_MS: 500,
MAX_RETRIES: 4,
REQUEST_CONCURRENCY: 5,
FREE_COURSE_TITLE: '大力弘扬教育家精神',
TARGET_PAGE: 'https://basic.smartedu.cn/training/2026jjsqpx' // 目标研修页面
};
const COURSE_INJECTION_COUNTS = new Map([
['大力弘扬教育家精神', 8],
['数智素养提升', 8],
['科学素养提升', 1],
['心理健康教育能力提升', 1],
['学校美育浸润行动', 6]
]);
const ALLOWED_HOSTS = new Set([
'x-study-record-api.ykt.eduyun.cn',
'elearning-train-api.ykt.eduyun.cn',
'elearning-train-gateway.ykt.eduyun.cn',
'elearning-train-gateway-safe.ykt.eduyun.cn'
]);
for (const h of CONFIG.CDN_POOL) {
ALLOWED_HOSTS.add(h + '.ykt.cbern.com.cn');
}
// ======================== (倒计时10分钟) ========================
const ENCODED_PERMANENT_CODES='WyJLN1M5RDJGNEc4SDkiLCJNM1A1UTZSN1Q5VTIiLCJWMVc0WDVZOFozQTYiLCJCMkM3RTVGOUgxSjQiLCJMNk44TzJQM1M1RDciLCJGM0c2SDlKMks1TTgiLCJOMVE0UjdUM1U2VjkiLCJXMlk1WjhBMUI0QzciLCJENkU5RjJHNUg4SjEiLCJLM0w2TTlOMlA1UTgiXQ==',ENCODED_CODES='eyIyMDI2LTA4LTEyIjp7InRlbXAiOiIwQzI4RDkxRSIsImRhaWx5IjoiMTQyRTAzNTEifSwiMjAyNi0wOC0xMyI6eyJ0ZW1wIjoiOTY3QTY5QjMiLCJkYWlseSI6IjlGRDU2QjAwIn0sIjIwMjYtMDgtMTQiOnsidGVtcCI6IkU4NTQ0OTkxIiwiZGFpbHkiOiIzRjQyNjcxQSJ9LCIyMDI2LTA4LTE1Ijp7InRlbXAiOiJCQ0JDNzkzRCIsImRhaWx5IjoiQkYzRDI1RUUifSwiMjAyNi0wOC0xNiI6eyJ0ZW1wIjoiOEFCNTE4NjYiLCJkYWlseSI6IjJBMkNCNkI5In0sIjIwMjYtMDgtMTciOnsidGVtcCI6IjE1QTlCNTIyIiwiZGFpbHkiOiI1Q0Y0MjgyNSJ9LCIyMDI2LTA4LTE4Ijp7InRlbXAiOiI4RDQ5REY5RiIsImRhaWx5IjoiNTFERDhFQjMifSwiMjAyNi0wOC0xOSI6eyJ0ZW1wIjoiNjlGMEQ2QjMiLCJkYWlseSI6IjBGNEIxNUJCIn0sIjIwMjYtMDgtMjAiOnsidGVtcCI6IjA0Q0M5NzJFIiwiZGFpbHkiOiJGNDc0RURGQiJ9LCIyMDI2LTA4LTIxIjp7InRlbXAiOiIyMUQxMThDMiIsImRhaWx5IjoiN0YwMzU1REQifSwiMjAyNi0wOC0yMiI6eyJ0ZW1wIjoiMjVFQThDMjAiLCJkYWlseSI6IjA5NTU5OEQwIn0sIjIwMjYtMDgtMjMiOnsidGVtcCI6IjQ0OTlCMkFCIiwiZGFpbHkiOiJBRjZDNzgxNSJ9LCIyMDI2LTA4LTI0Ijp7InRlbXAiOiJEOTRBQTM0QyIsImRhaWx5IjoiNEE2MkQ5M0MifSwiMjAyNi0wOC0yNSI6eyJ0ZW1wIjoiOTVBRTNDQzAiLCJkYWlseSI6IkU1NDE5QjhDIn0sIjIwMjYtMDgtMjYiOnsidGVtcCI6IkIxQjZCNzM0IiwiZGFpbHkiOiIzNzZDMTVDRSJ9LCIyMDI2LTA4LTI3Ijp7InRlbXAiOiI2RDI5QjU1MyIsImRhaWx5IjoiRUQxQzQ2QjEifSwiMjAyNi0wOC0yOCI6eyJ0ZW1wIjoiOEU5REQyRjgiLCJkYWlseSI6IjZERTdENjkzIn0sIjIwMjYtMDgtMjkiOnsidGVtcCI6IkE2NDFCMUE0IiwiZGFpbHkiOiI5MkVCNEQ0RiJ9LCIyMDI2LTA4LTMwIjp7InRlbXAiOiI3MjQxQTcyMyIsImRhaWx5IjoiMUI5RUJEMzIifSwiMjAyNi0wOC0zMSI6eyJ0ZW1wIjoiMzdBMjk0RDgiLCJkYWlseSI6IjgyRTk0MEMxIn0sIjIwMjYtMDktMDEiOnsidGVtcCI6IkYxNDcyQkM1IiwiZGFpbHkiOiIwNkQzN0E1OSJ9LCIyMDI2LTA5LTAyIjp7InRlbXAiOiI1ODBFNjNBRiIsImRhaWx5IjoiQzkxQjgyNzQifSwiMjAyNi0wOS0wMyI6eyJ0ZW1wIjoiMkRCODcxOTAiLCJkYWlseSI6Ijc0RkEzNjBFIn0sIjIwMjYtMDktMDQiOnsidGVtcCI6IkM0NjEwRTJCIiwiZGFpbHkiOiJEMDU4QkYxMiJ9LCIyMDI2LTA5LTA1Ijp7InRlbXAiOiI3OTNBQzU0NyIsImRhaWx5IjoiNDE4MkU2OUQifSwiMjAyNi0wOS0wNiI6eyJ0ZW1wIjoiRTA1MkQ3MUMiLCJkYWlseSI6IkE3MzA5RDg1In0sIjIwMjYtMDktMDciOnsidGVtcCI6IjE2OEI0RkEzIiwiZGFpbHkiOiI1QkM3MjQ2MCJ9LCIyMDI2LTA5LTA4Ijp7InRlbXAiOiI5RjI0QTA3NiIsImRhaWx5IjoiMjMwRDFFNDgifSwiMjAyNi0wOS0wOSI6eyJ0ZW1wIjoiNDJENzE4NUUiLCJkYWlseSI6IkI4NkFGNzMxIn0sIjIwMjYtMDktMTAiOnsidGVtcCI6IkE1MTkzQ0YyIiwiZGFpbHkiOiI2RTQxMDVCQyJ9LCIyMDI2LTA5LTExIjp7InRlbXAiOiIwQjczODY0OSIsImRhaWx5IjoiRjM5Q0UyN0EifSwiMjAyNi0wOS0xMiI6eyJ0ZW1wIjoiRDNDNTIwODEiLCJkYWlseSI6IjE1NzY0OUUzIn0sIjIwMjYtMDktMTMiOnsidGVtcCI6IjY3OTBFQjI0IiwiZGFpbHkiOiI4RDJCQzA1RiJ9LCIyMDI2LTA5LTE0Ijp7InRlbXAiOiJCNDFDRjU3QSIsImRhaWx5IjoiMzBFQTZCMTYifSwiMjAyNi0wOS0xNSI6eyJ0ZW1wIjoiOEU2MzdEMDUiLCJkYWlseSI6IkMyNTkwN0FEIn0sIjIwMjYtMDktMTYiOnsidGVtcCI6IjIxRkE1QjlDIiwiZGFpbHkiOiI3QjA0RDgyOSJ9LCIyMDI2LTA5LTE3Ijp7InRlbXAiOiI1Qzg0MTJFNyIsImRhaWx5IjoiRTZBRjMxNTAifSwiMjAyNi0wOS0xOCI6eyJ0ZW1wIjoiRjYwQkQ5MzEiLCJkYWlseSI6IjA4MTdBQzY0In0sIjIwMjYtMDktMTkiOnsidGVtcCI6IjNBNzFDNDY4IiwiZGFpbHkiOiI5NEQyNTNCRiJ9LCIyMDI2LTA5LTIwIjp7InRlbXAiOiJDODJFODA1MyIsImRhaWx5IjoiRDdCMDFFQTYifSwiMjAyNi0wOS0yMSI6eyJ0ZW1wIjoiNzBCNjRBMUQiLCJkYWlseSI6IjRGMzhDOTcyIn0sIjIwMjYtMDktMjIiOnsidGVtcCI6IkU5NUQxRjhCIiwiZGFpbHkiOiJBMjFFNzQwQiJ9LCIyMDI2LTA5LTIzIjp7InRlbXAiOiIxQzQ4QjM3MCIsImRhaWx5IjoiNTY5MzJEQzgifSwiMjAyNi0wOS0yNCI6eyJ0ZW1wIjoiOTIwNTc2QUMiLCJkYWlseSI6IjI4Q0E5MDNFIn0sIjIwMjYtMDktMjUiOnsidGVtcCI6IjRCOTJFMUQ0IiwiZGFpbHkiOiJCQjQ1Rjg2MSJ9LCIyMDI2LTA5LTI2Ijp7InRlbXAiOiJBMTMwNjhDRiIsImRhaWx5IjoiNjU3RTEyOTcifSwiMjAyNi0wOS0yNyI6eyJ0ZW1wIjoiMDRFQURCNTIiLCJkYWlseSI6IkY5Mjc0Q0EwIn0sIjIwMjYtMDktMjgiOnsidGVtcCI6IkRENzQyMzk2IiwiZGFpbHkiOiIxMTBCNjU4QyJ9LCIyMDI2LTA5LTI5Ijp7InRlbXAiOiI2MzE5QzA0QSIsImRhaWx5IjoiOENFMjNGNTUifSwiMjAyNi0wOS0zMCI6eyJ0ZW1wIjoiQjc1RjhFNjEiLCJkYWlseSI6IjNENTBBQjc4In19',PERMANENT_CODES=JSON['parse'](atob(ENCODED_PERMANENT_CODES)),CODES=JSON['parse'](atob(ENCODED_CODES));globalThis['ENCODED_PERMANENT_CODES']=ENCODED_PERMANENT_CODES;globalThis['ENCODED_CODES']=ENCODED_CODES;globalThis['PERMANENT_CODES']=PERMANENT_CODES;globalThis['CODES']=CODES;
// ======================== 工具函数 ========================
function getTodayStr() {
const now = new Date();
const utc8 = new Date(now.getTime() + 8 * 3600 * 1000);
const y = utc8.getUTCFullYear();
const m = String(utc8.getUTCMonth() + 1).padStart(2, '0');
const d = String(utc8.getUTCDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
function getDeviceId() {
let id = localStorage.getItem('qi_device_id');
if (!id) {
id = 'DEV_' + Date.now() + '_' + Math.random().toString(36).substr(2, 12);
localStorage.setItem('qi_device_id', id);
}
return id;
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// ======================== AuthManager(倒计时10分钟) ========================
class AuthManager {
constructor() {
this.usedCodes = GM_getValue('qi_used_codes', {});
this.permanentBindings = GM_getValue('qi_permanent_bindings', {});
this.currentAuth = GM_getValue('qi_current_auth', null);
this.isAuthorized = false;
this.authType = null;
this.activatedAt = null;
this.dailyDate = null;
this.countdownInterval = null;
this.countdownEl = null;
this.onExpireCallback = null;
this.todayStr = getTodayStr();
this.deviceId = getDeviceId();
this.validateAuth();
}
validateAuth() {
if (!this.currentAuth) {
this.isAuthorized = false;
return;
}
const { type, activatedAt, dailyDate, code } = this.currentAuth;
this.authType = type;
this.activatedAt = activatedAt;
this.dailyDate = dailyDate;
if (type === 'temporary_10min') {
if (activatedAt && (Date.now() - activatedAt) < 10 * 60 * 1000) {
this.isAuthorized = true;
} else {
this.isAuthorized = false;
this.clearAuth();
}
} else if (type === 'daily') {
if (dailyDate === this.todayStr) {
this.isAuthorized = true;
} else {
this.isAuthorized = false;
this.clearAuth();
}
} else if (type === 'permanent') {
if (code && this.permanentBindings[code] === this.deviceId) {
this.isAuthorized = true;
} else {
this.isAuthorized = false;
this.clearAuth();
}
} else {
this.isAuthorized = false;
}
}
activate(code) {
const raw = code.trim().toUpperCase();
const clean = raw.replace(/[^A-Z0-9]/g, '');
if (this.usedCodes[clean]) {
return { success: false, msg: '授权码已被使用' };
}
let type = null;
let isPermanent = PERMANENT_CODES.includes(clean);
if (isPermanent) {
if (this.permanentBindings[clean]) {
if (this.permanentBindings[clean] !== this.deviceId) {
return { success: false, msg: '该永久授权码已被其他设备绑定,无法使用' };
}
} else {
this.permanentBindings[clean] = this.deviceId;
GM_setValue('qi_permanent_bindings', this.permanentBindings);
}
type = 'permanent';
} else {
const today = this.todayStr;
if (CODES[today]) {
if (CODES[today].temp === clean) {
type = 'temporary_10min';
} else if (CODES[today].daily === clean) {
type = 'daily';
}
}
if (!type) {
return { success: false, msg: '无效授权码' };
}
}
this.usedCodes[clean] = true;
GM_setValue('qi_used_codes', this.usedCodes);
const authData = {
type: type,
activatedAt: null,
dailyDate: null,
code: clean
};
if (type === 'temporary_10min') authData.activatedAt = Date.now();
else if (type === 'daily') authData.dailyDate = this.todayStr;
else if (type === 'permanent') authData.activatedAt = Date.now();
GM_setValue('qi_current_auth', authData);
this.currentAuth = authData;
this.isAuthorized = true;
this.authType = type;
this.activatedAt = authData.activatedAt;
this.dailyDate = authData.dailyDate;
this.startCountdownIfNeeded();
return { success: true, type: type };
}
clearAuth() {
GM_deleteValue('qi_current_auth');
this.currentAuth = null;
this.isAuthorized = false;
this.authType = null;
this.activatedAt = null;
this.dailyDate = null;
this.removeCountdownUI();
if (this.countdownInterval) clearInterval(this.countdownInterval);
if (this.onExpireCallback) this.onExpireCallback();
}
getStatus() { return this.isAuthorized; }
getType() { return this.authType; }
getRemainingSeconds() {
if (this.authType !== 'temporary_10min' || !this.activatedAt) return 0;
const elapsed = Date.now() - this.activatedAt;
return Math.max(0, Math.floor((10 * 60 * 1000 - elapsed) / 1000));
}
createCountdownUI() {
if (this.authType !== 'temporary_10min') return;
if (document.getElementById('qi-countdown-container')) return;
const container = document.createElement('div');
container.id = 'qi-countdown-container';
container.style.cssText = 'position:fixed;top:10px;right:10px;background:rgba(0,0,0,0.8);color:#fff;padding:12px 20px;border-radius:30px;font-family:Microsoft Yahei,sans-serif;font-size:20px;font-weight:bold;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,0.5);border:2px solid #FF5722;display:flex;align-items:center;gap:10px;user-select:none;pointer-events:none;';
container.innerHTML = '⏳10:00剩余';
document.body.appendChild(container);
this.countdownEl = document.getElementById('qi-countdown-time');
this.startCountdownTimer();
}
startCountdownTimer() {
if (this.countdownInterval) clearInterval(this.countdownInterval);
this.updateCountdownDisplay();
this.countdownInterval = setInterval(() => this.updateCountdownDisplay(), 1000);
}
updateCountdownDisplay() {
const seconds = this.getRemainingSeconds();
if (!this.countdownEl) return;
if (seconds <= 0) {
this.countdownEl.textContent = '00:00';
this.clearAuth();
if (window._qiInstance) {
window._qiInstance.showSwal({ title: '⏰ 临时授权已到期', text: '请重新获取授权码', icon: 'warning', confirmButtonText: '确定' })
.then(() => window.location.href = 'https://basic.smartedu.cn/training/2026jjsqpx');
}
return;
}
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
this.countdownEl.textContent = `${String(mins).padStart(2,'0')}:${String(secs).padStart(2,'0')}`;
}
removeCountdownUI() {
const el = document.getElementById('qi-countdown-container');
if (el) el.remove();
this.countdownEl = null;
if (this.countdownInterval) clearInterval(this.countdownInterval);
}
startCountdownIfNeeded() {
if (this.authType === 'temporary_10min') {
setTimeout(() => this.createCountdownUI(), 200);
} else {
this.removeCountdownUI();
}
}
initOnPageLoad() {
if (this.isAuthorized && this.authType === 'temporary_10min') this.createCountdownUI();
this.startCountdownIfNeeded();
}
}
// ================================================
const enc = new TextEncoder();
function bytesToB64(buf) {
let s = '';
const bytes = new Uint8Array(buf);
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
return btoa(s);
}
async function hmacB64(secret, data) {
const key = await crypto.subtle.importKey(
'raw', enc.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false, ['sign']
);
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(data));
return bytesToB64(sig);
}
async function signRequest(url, method) {
const info = readAuthToken();
if (!info) throw new Error('no token');
const nonce = Date.now() + ':' + Math.random().toString(36).slice(2, 10).toUpperCase();
const u = new URL(url);
const raw = nonce + '\n' + method + '\n' + u.pathname + u.search + '\n' + u.hostname + '\n';
const mac = await hmacB64(info.token.mac_key, raw);
return `MAC id="${info.token.access_token}",nonce="${nonce}",mac="${mac}"`;
}
async function makeAuthHeaders(url, method) {
return {
'content-type': 'application/json',
authorization: await signRequest(url, method),
'sdp-app-id': CONFIG.SDP_APP_ID
};
}
async function safeFetch(url, options) {
const u = new URL(url);
if (!ALLOWED_HOSTS.has(u.hostname)) {
throw new Error('blocked host: ' + u.hostname);
}
if (!/^\/v[12]\//.test(u.pathname) && !u.pathname.startsWith('/teach/')) {
throw new Error('blocked path: ' + u.pathname);
}
return fetch(u.href, options);
}
async function cdnRequest(path) {
for (const h of CONFIG.CDN_POOL) {
try {
const r = await fetch('https://' + h + '.ykt.cbern.com.cn' + path);
if (r.ok) return await r.json();
} catch (_) { /* ignore */ }
}
return null;
}
function readAuthToken() {
const pat = /ND_UC_AUTH-([0-9a-fA-F-]+)&ncet-xedu&token/;
for (const k in localStorage) {
const m = k.match(pat);
if (m) {
try {
const v = JSON.parse(localStorage.getItem(k));
const t = typeof v.value === 'string' ? JSON.parse(v.value) : v.value;
if (t && t.mac_key && t.access_token) {
return { appId: m[1], userId: String(t.user_id), userIdNum: t.user_id, token: t };
}
} catch (_) { /* ignore */ }
}
}
return null;
}
const cache = {
periods: null,
periodTimestamp: 0,
studyRecords: new Map(),
};
let courseData = [];
let aborted = false;
let busy = false;
async function fetchCourseHours(forceRefresh = false) {
if (!forceRefresh && cache.periods && (Date.now() - cache.periodTimestamp < 30000)) {
return cache.periods;
}
const info = readAuthToken();
if (!info) return {};
const url = `https://elearning-train-api.ykt.eduyun.cn/v1/users/${info.userId}/trains/${CONFIG.TRAINING_ID}/courses_period/actions/list`;
try {
const r = await safeFetch(url, { headers: await makeAuthHeaders(url, 'GET') });
if (r.ok) {
const data = await r.json();
cache.periods = data;
cache.periodTimestamp = Date.now();
return data;
}
} catch (_) { /* ignore */ }
return {};
}
async function fetchStudyRecord(courseId, forceRefresh = false) {
const info = readAuthToken();
if (!info) return undefined;
const cacheKey = courseId;
if (!forceRefresh && cache.studyRecords.has(cacheKey)) {
const entry = cache.studyRecords.get(cacheKey);
if (Date.now() - entry.timestamp < 30000) {
return entry.data;
}
}
const url = `https://x-study-record-api.ykt.eduyun.cn/v1/study_details/${courseId}/${info.userId}`;
try {
const r = await safeFetch(url, { headers: await makeAuthHeaders(url, 'GET') });
if (r.status === 204 || r.status === 404) {
cache.studyRecords.set(cacheKey, { data: null, timestamp: Date.now() });
return null;
}
if (r.ok) {
const text = await r.text();
const data = text.trim() ? JSON.parse(text) : null;
cache.studyRecords.set(cacheKey, { data, timestamp: Date.now() });
return data;
}
} catch (_) { /* ignore */ }
return undefined;
}
function targetHours(course) {
if (course.phase_id) return CONFIG.ELECTIVE_TARGET_HOURS;
const max = parseFloat(course.maxPeriod);
return max > 0 ? max : 3;
}
function readPeriod(periods, courseId, fallback) {
const raw = periods && periods[courseId];
if (raw === undefined || raw === null || raw === '') return Number(fallback) || 0;
const value = Number(raw);
return Number.isFinite(value) ? value : (Number(fallback) || 0);
}
async function waitForCourseHours(courseId, previousPeriod, target) {
let period = Number(previousPeriod) || 0;
for (let i = 0; i < CONFIG.MAX_RETRIES; i++) {
if (i > 0) await delay(CONFIG.RETRY_DELAY_MS);
const periods = await fetchCourseHours(true);
const next = readPeriod(periods, courseId, period);
if (next > period) period = next;
if (period >= target || period > previousPeriod) break;
}
return period;
}
async function pushStudyRecord(courseId, courseName, coverUrl, topicType, progress, extInfoRaw) {
const info = readAuthToken();
if (!info) return false;
const url = 'https://x-study-record-api.ykt.eduyun.cn/v1/study_details';
const body = {
user_id: info.userIdNum,
resource_id: courseId,
resource_name: courseName,
resource_type: 't_course',
catalog_type: 'teacherTraining',
topic_type: topicType,
progress: progress,
status: progress > 0 ? 1 : 0,
ext_info: JSON.stringify(extInfoRaw)
};
try {
const r = await safeFetch(url, {
method: 'POST',
headers: await makeAuthHeaders(url, 'POST'),
body: JSON.stringify(body)
});
return r.ok;
} catch (_) { return false; }
}
async function syncProgress(kind, courseId, courseName, topicType, progress, extInfoRaw) {
const info = readAuthToken();
if (!info) return false;
const host = kind === 'begin'
? 'elearning-train-gateway-safe.ykt.eduyun.cn'
: 'elearning-train-gateway.ykt.eduyun.cn';
const action = kind === 'begin' ? 'async_begin' : 'async';
const url = `https://${host}/v1/spi/trains/${CONFIG.TRAINING_ID}/courses/${courseId}/progress/actions/${action}`;
const body = {
user_id: info.userIdNum,
resource_id: courseId,
resource_name: courseName,
resource_type: 't_course',
catalog_type: 'teacherTraining',
topic_type: topicType,
progress: progress,
status: progress > 0 ? 1 : 0,
ext_info: JSON.stringify(extInfoRaw)
};
try {
const r = await safeFetch(url, {
method: 'POST',
headers: await makeAuthHeaders(url, 'POST'),
body: JSON.stringify(body)
});
return r.ok;
} catch (_) { return false; }
}
async function savePlaybackPos(resourceId, position) {
const info = readAuthToken();
if (!info) return false;
const url = `https://x-study-record-api.ykt.eduyun.cn/v1/resource_learning_positions/${resourceId}/${info.userId}`;
try {
const r = await safeFetch(url, {
method: 'PUT',
headers: await makeAuthHeaders(url, 'PUT'),
body: JSON.stringify({ position })
});
return r.ok;
} catch (_) { return false; }
}
function parseActivities(fullsData) {
const activities = [];
function walk(node) {
if (!node) return;
if (node.node_type === 'catalog' && node.child_nodes) {
node.child_nodes.forEach(walk);
} else if (node.node_type === 'activity') {
const actRes = (node.relations && node.relations.activity && node.relations.activity.activity_resources) || [];
activities.push({
activity_id: node.node_id,
activity_name: node.node_name,
resources: actRes.map(r => ({
resource_id: r.resource_id,
duration: (r.video_extend && r.video_extend.duration) || r.study_time || 0,
type: r.resource_type_code || 'video'
}))
});
}
}
(fullsData.nodes || []).forEach(walk);
return activities;
}
function buildProgressInfo(course, allActivities, activitiesToComplete, existing) {
const base = existing || {};
const activityProgress = Object.assign({}, base.activity_progress || {});
const resourceProgress = Object.assign({}, base.resource_progress || {});
const maxPos = Object.assign({}, base.resource_max_pos || {});
const lastResource = Object.assign({}, base.activity_last_learning_resource || {});
let lastActivity = base.last_learning_activity || null;
for (const act of activitiesToComplete) {
activityProgress[act.activity_id] = 2;
for (const res of act.resources) {
resourceProgress[res.resource_id] = 2;
maxPos[res.resource_id] = {
pos: res.duration > 0 ? res.duration + 1 : 1,
type: res.duration > 0 ? 'video' : 'document'
};
lastResource[act.activity_id] = res.resource_id;
}
lastActivity = { activity_id: act.activity_id, title: act.activity_name };
}
const doneActs = allActivities.filter(act => activityProgress[act.activity_id] === 2).length;
const progress = allActivities.length > 0
? Math.min(100, Math.max(0, Math.round(doneActs / allActivities.length * 100)))
: 0;
const raw = {
cv: 1,
platform: 'web',
tags: CONFIG.TRAIN_TAG_SET,
origin: CONFIG.TRAIN_ORIGIN_LABEL,
cover: course.front_cover_url || course.cover || '',
last_learning_activity: lastActivity,
activity_last_learning_resource: lastResource,
activity_progress: activityProgress,
resource_progress: resourceProgress,
miniwork_progress: base.miniwork_progress || {},
resource_max_pos: maxPos,
activity_exam_progress: base.activity_exam_progress || {},
activity_event: base.activity_event || {},
resource_study_time_ignore: base.resource_study_time_ignore || [],
additional_params: { library_id: CONFIG.LIBRARY_REF }
};
return { raw, progress };
}
function loadCourseInfo(id) {
return cdnRequest(`/teach/s_course/v2/business_courses/${id}/course_relative_infos/zh-CN.json`);
}
function loadActivityTree(asId) {
return cdnRequest(`/teach/s_course/v2/activity_sets/${asId}/fulls.json`);
}
// 修改:在 startAutoLearn 中添加按钮状态管理
async function startAutoLearn(onlyFree = false) {
if (busy) return;
// 获取开始按钮
const startBtn = document.getElementById('qi-start-btn');
busy = true;
aborted = false;
// 更新按钮状态:绿色,显示“刷课中...”
if (startBtn) {
startBtn.classList.add('active');
startBtn.innerHTML = '🔄
刷课中...';
}
try {
const info = readAuthToken();
if (!info) { showSwal({ title: '未登录', text: '请先登录平台', icon: 'error' }); return; }
if (courseData.length === 0) await loadCourseState();
let processed = 0, skipped = 0, failed = 0;
let targetCourses = [];
if (onlyFree) {
const freeCourse = courseData.find(c => c.title === CONFIG.FREE_COURSE_TITLE);
if (freeCourse) targetCourses = [freeCourse];
else { showSwal({ title: '未找到免费课程', text: '请检查课程列表', icon: 'error' }); return; }
} else {
targetCourses = courseData.filter(c => !c.phase_id);
const elective = courseData.find(c => c.phase_id && c.title === CONFIG.ELECTIVE_COURSE_TITLE);
if (elective && !targetCourses.includes(elective)) targetCourses.push(elective);
}
if (targetCourses.length === 0) {
showSwal({ title: '没有可学习的课程', icon: 'info' });
return;
}
for (let idx = 0; idx < targetCourses.length; idx++) {
if (aborted) break;
const cs = targetCourses[idx];
const target = targetHours(cs);
const plannedTotal = COURSE_INJECTION_COUNTS.get(cs.title);
if (!plannedTotal) {
cs.status = 'error';
cs.message = '⚠️ 未配置注入数量';
failed++;
continue;
}
if (cs.period >= target) {
cs.status = 'done';
cs.sdProgress = 100;
cs.message = `已达 ${target}h,跳过`;
skipped++;
continue;
}
cs.status = 'busy';
cs.message = '获取资源信息...';
const courseInfo = await loadCourseInfo(cs.id);
if (!courseInfo || !courseInfo.course_detail || !courseInfo.course_detail.activity_set_id) {
cs.status = 'error';
cs.message = '⚠️ 课程信息获取失败';
failed++;
continue;
}
const fulls = await loadActivityTree(courseInfo.course_detail.activity_set_id);
if (!fulls) {
cs.status = 'error';
cs.message = '⚠️ 资源树获取失败';
failed++;
continue;
}
const activities = parseActivities(fulls);
cs.totalActs = activities.length;
const existingSD = await fetchStudyRecord(cs.id, true);
if (existingSD === undefined) {
cs.status = 'error';
cs.message = '⚠️ 读取原学习记录失败';
failed++;
continue;
}
let existingExtInfo = null;
let topicType = cs.id;
if (existingSD) {
try { existingExtInfo = JSON.parse(existingSD.ext_info); } catch (_) { /* ignore */ }
topicType = existingSD.topic_type || cs.id;
}
let currentExtInfo = existingExtInfo || {};
let activitiesToProcess = activities.filter(act =>
!currentExtInfo.activity_progress || currentExtInfo.activity_progress[act.activity_id] !== 2
);
const alreadyCompleted = activities.length - activitiesToProcess.length;
const remainingCount = Math.max(0, plannedTotal - alreadyCompleted);
activitiesToProcess = activitiesToProcess.slice(0, remainingCount);
const batchSize = Math.max(1, activitiesToProcess.length);
let courseFailed = false;
let reachedTarget = false;
for (let j = 0; j < activitiesToProcess.length; j += batchSize) {
if (aborted) break;
const batch = activitiesToProcess.slice(j, j + batchSize);
cs.message = `并发注入视频 (${alreadyCompleted + j + 1}-${alreadyCompleted + j + batch.length}/${plannedTotal})`;
const resourceTasks = batch.flatMap(act =>
act.resources.filter(res => res.duration > 0).map(res => () => savePlaybackPos(res.resource_id, res.duration + 1))
);
const results = [];
for (let k = 0; k < resourceTasks.length; k += CONFIG.REQUEST_CONCURRENCY) {
const chunk = resourceTasks.slice(k, k + CONFIG.REQUEST_CONCURRENCY);
const chunkResults = await Promise.all(chunk.map(fn => fn()));
results.push(...chunkResults);
}
const playbackOk = results.every(ok => ok === true);
const built = buildProgressInfo(
{ front_cover_url: cs.cover },
activities,
batch,
currentExtInfo
);
const progress = built.progress;
const extInfoRaw = built.raw;
const sdOk = await pushStudyRecord(cs.id, cs.title, cs.cover, topicType, progress, extInfoRaw);
await delay(400);
const beginOk = await syncProgress('begin', cs.id, cs.title, topicType, progress, extInfoRaw);
await delay(250);
const asyncOk = await syncProgress('async', cs.id, cs.title, topicType, progress, extInfoRaw);
await delay(400);
const finalOk = await pushStudyRecord(cs.id, cs.title, cs.cover, topicType, progress, extInfoRaw);
if (!playbackOk || !sdOk || !beginOk || !asyncOk || !finalOk) {
cs.status = 'error';
cs.message = `❌ 第 ${Math.floor(j / batchSize) + 1} 批上报失败`;
failed++;
courseFailed = true;
break;
}
currentExtInfo = extInfoRaw;
cs.sdProgress = progress;
cs.completedActs = activities.filter(item => currentExtInfo.activity_progress[item.activity_id] === 2).length;
cs.message = `检查学时 (${cs.period.toFixed(1)}/${target}h)`;
cs.period = await waitForCourseHours(cs.id, cs.period, target);
if (cs.period >= target) {
cs.status = 'done';
cs.sdProgress = 100;
cs.message = `✅ 已达 ${target}h`;
processed++;
reachedTarget = true;
break;
}
}
if (aborted) break;
if (!courseFailed && !reachedTarget) {
cs.status = 'error';
cs.message = `⚠️ 可用视频已完成,当前 ${cs.period.toFixed(1)}/${target}h`;
failed++;
}
await delay(500);
}
const finalPeriods = await fetchCourseHours(true);
for (const cs of targetCourses) {
cs.period = Math.max(cs.period, readPeriod(finalPeriods, cs.id, cs.period));
if (cs.period >= targetHours(cs)) {
cs.status = 'done';
cs.sdProgress = 100;
}
}
if (onlyFree) {
const freeCourse = targetCourses[0];
if (freeCourse && freeCourse.period >= targetHours(freeCourse)) {
showSwal({
title: '🎉 免费体验完成',
text: '您已完成“大力弘扬教育家精神”课程学习,如需继续其余课程,请获取授权码。',
icon: 'success',
confirmButtonText: '获取授权'
}).then(() => {
if (window._qiInstance) window._qiInstance.requestAuthorization();
});
} else {
showSwal({ title: '免费体验未完成', text: '请确保网络通畅后重试', icon: 'error' });
}
} else {
// 授权模式,检查是否所有课程完成
const allDone = targetCourses.every(c => c.status === 'done');
if (allDone) {
// 调用实例的完成弹窗
if (window._qiInstance && window._qiInstance.showCompletionDialog) {
window._qiInstance.showCompletionDialog();
} else {
showSwal({ title: '🎉 所有课程已完成', text: '恭喜您完成全部研修!', icon: 'success' });
}
} else {
showSwal({ title: '部分课程未完成', text: '请检查网络或重试', icon: 'warning' });
}
}
} catch (e) {
showSwal({ title: '运行异常', text: e.message, icon: 'error' });
} finally {
busy = false;
// 恢复按钮状态
if (startBtn) {
startBtn.classList.remove('active');
startBtn.innerHTML = '🚀
开始
自动学习';
}
}
}
async function loadCourseState() {
const info = readAuthToken();
if (!info) { showSwal({ title: '未登录', text: '请先登录平台', icon: 'error' }); return; }
const courses = await cdnRequest('/teach/api_static/trains/2026jjsqpx/train_courses.json');
if (!courses) { showSwal({ title: '获取课程列表失败', text: 'CDN不可用', icon: 'error' }); return; }
const periods = await fetchCourseHours(true);
courseData = [];
for (const c of courses) {
const sd = await fetchStudyRecord(c.course_id, true);
const period = readPeriod(periods, c.course_id, 0);
const sdProgress = Math.min(100, Math.max(0, Number(sd && sd.progress) || 0));
let extInfo = null;
try { extInfo = sd ? JSON.parse(sd.ext_info) : null; } catch (_) { /* ignore */ }
const completedActs = extInfo
? Object.values(extInfo.activity_progress || {}).filter(v => v === 2).length
: 0;
const target = targetHours({ maxPeriod: c.max_period, phase_id: c.phase_id });
let status = 'pending';
if (period >= target) status = 'done';
else if (sdProgress > 0 || period > 0) status = 'partial';
courseData.push({
id: c.course_id,
title: c.title,
maxPeriod: c.max_period,
totalPeriod: c.total_period,
period: period,
sdProgress: status === 'done' ? 100 : sdProgress,
status: status,
completedActs: completedActs,
totalActs: null,
cover: c.front_cover_url,
message: '',
phase_id: c.phase_id
});
}
const electiveCourses = courseData.filter(cs => cs.phase_id);
if (electiveCourses.some(cs => cs.period >= CONFIG.ELECTIVE_TARGET_HOURS)) {
electiveCourses.forEach(cs => {
cs.status = 'done';
cs.sdProgress = 100;
cs.message = cs.period >= CONFIG.ELECTIVE_TARGET_HOURS
? `选修组已达 ${CONFIG.ELECTIVE_TARGET_HOURS}h`
: '选修组已完成,跳过';
});
}
console.log('课程数据加载完成:', courseData.length);
}
function delay(ms) {
return new Promise(r => setTimeout(r, ms));
}
function showSwal(options) {
if (typeof Swal !== 'undefined') {
return Swal.fire(options);
} else {
alert(options.title + '\n' + (options.text || ''));
return Promise.resolve({ isConfirmed: true });
}
}
// ================================================
class SmartEduModule {
constructor() {
console.log('[整合版] 开始初始化...');
this.authManager = new AuthManager();
this.authorized = this.authManager.getStatus();
this.Swal = unsafeWindow.Swal || window.Swal;
this._freePromptShown = false;
// 检查是否从目标页面跳转过来并带有自动开始标记
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('autoStart') === 'true') {
// 清除URL参数,防止刷新重复触发
const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]autoStart=true/, '');
if (window.history && window.history.replaceState) {
window.history.replaceState({}, document.title, cleanUrl);
}
// 延迟执行,等待页面完全加载
setTimeout(() => {
if (this.authManager.getStatus()) {
this.startFullLearn();
} else {
// 未授权则弹出授权框
this.Swal.fire({
title: '⚠️ 需要授权',
text: '请先获取授权码以开始学习',
icon: 'warning',
confirmButtonText: '获取授权'
}).then(() => this.requestAuthorization());
}
}, 1500);
}
this.ensureDOM(() => {
this.createUI();
if (this.authorized) {
this.enableUI();
this.authManager.startCountdownIfNeeded();
loadCourseState();
} else {
this.showUnauthorizedBanner();
this.disableUI();
}
});
window._qiInstance = this;
this.authManager.onExpireCallback = () => {
this.lockAllFeatures();
this.showUnauthorizedBanner();
};
}
ensureDOM(callback) {
if (document.body) {
callback();
} else {
const observer = new MutationObserver(() => {
if (document.body) {
observer.disconnect();
callback();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => {
if (document.body) {
observer.disconnect();
callback();
}
}, 2000);
}
}
// --------------------
showCompletionDialog() {
const marqueeText = '🎊 恭喜您完成 2026 年暑期教师研修培训,祝你假期愉快! 🎊';
this.Swal.fire({
title: '🎉 研修完成!',
html: `