// ==UserScript== // @name 【完全免费】2026年基础教育暑期教师研修·一键秒学助手 // @namespace sumuve.local // @version 1.0.1 // @description 一键完成2026年基础教育暑期教师研修全部学时(完全免费·本地运行) // @author sumuve // @icon data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7GIebOxiHuPsYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe4+xiHmwAAAAA7GIebOxiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIebOxiHuPsYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHuPsYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xjH//2tJP//vj1///////+8+3/+dC8//KTZf/sZCH/7GIe/+xiHv/taCb/7GIe/+xiHv/sYh7/7GIe/+94Pf/61cL///////3x6//61sX/+c65//ve0P/97+j//fDq//nPuv/1rYr/73c8/+xiHv/sYh7/7GIe/+xjH//85dr///////zp3//wgUr/7GIe/+96QP/++vj///v6//Wui//sZCL/7GIe/+xiHv/sYh7/7GIe/+xiHv/uczb////////////97OT/9riZ/+xiHv/wgkz/////////////////735G/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GYj//728v///////Orh//jHr//udjr/7GIe//nNuP////////////ObcP/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/zmGz//vn2///7+v///fv/+tXD//vdzv/////////////+/v/ve0H/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe//KRYv////////////////////////7+//3x7P/zoHb/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/9a6L//728//++ff/9raX/+xkIv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GMg/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe4+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe4+xiHmzsYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHmwAAAAA7GIebOxiHuPsYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe/+xiHv/sYh7/7GIe4+xiHmwAAAAAwAMAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAAwAMAAA== // @match https://basic.smartedu.cn/* // @match https://teacher.ykt.eduyun.cn/* // @connect s-file-1.ykt.cbern.com.cn // @connect s-file-2.ykt.cbern.com.cn // @connect x-study-record-api.ykt.eduyun.cn // @connect elearning-train-gateway.ykt.eduyun.cn // @connect elearning-train-gateway-safe.ykt.eduyun.cn // @connect elearning-train-api.ykt.eduyun.cn // @connect x-api.ykt.eduyun.cn // @require https://fastly.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant unsafeWindow // @run-at document-end // @license MIT // @antifeature ads // @antifeature referral-link // @tag 暑期研修 秒学助手 智慧教育 教师培训 油猴脚本 // ==/UserScript== (function() { 'use strict'; // ==================== 全局配置 ==================== const CONFIG = { TRAIN_ID: 'dc6d78f2-bad8-4d09-b8da-0d758803dbe4', LIBRARY_ID: 'bb042e69-9a11-49a1-af22-0c3fab2e92b9', TAG: '2026年"暑期教师研修"专题(基础教育)', TARGET_PERIOD: 10, PERIOD_RATIO: 2700, SDP_APP_ID: 'e5649925-441d-4a53-b525-51a2f1c4e0a8', AUTO_START_KEY: 'sumuve_auto_start', WELCOME_SHOWN_KEY: 'sumuve_welcome_shown_v1', PANEL_POS_KEY: 'sumuve_panel_pos', FLOW_STATE_KEY: 'sumuve_flow_state_v1', // 跨页面流程状态 PAUSED_KEY: 'sumuve_paused_v1', // 暂停状态持久化 STOPPED_KEY: 'sumuve_stopped_v1', // 停止状态持久化 LOGS_KEY: 'sumuve_logs_v1', // 日志持久化 PERIOD_ASKED_KEY: 'sumuve_period_asked_v1', // 学时达标是否已问过 SCRIPT_NAME: '一键秒学助手', AUTHOR_QR_BASE64: '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', }; // 11门课程清单(含详情页URL和首个资源ID) // maxPeriod: 认定学时要求(小时),从 train_courses.json 的 max_period 字段获取 // -1 表示选修课(按阶段汇总认定) // phaseId: 选修课阶段ID(从 train_courses.json 的 phase_id 字段获取) const TAG_ENCODED = encodeURIComponent(CONFIG.TAG); const ELECTIVE_PHASE_ID = 'd763f410-d875-4c11-a81e-dc92b6ae0788'; // 选修课阶段ID const COURSES = [ // 必修课(4门,共7学时) { id: '79d28f6c-0b8a-443d-bdda-a78165c462f3', name: '大力弘扬教育家精神', period: 2, maxPeriod: 2, phaseId: null, firstResourceId: '62be7ce3-c91b-43c3-9c8b-dba1ffcb9cdd' }, { id: '165d9433-5486-43e4-926e-3f33b92635e4', name: '数智素养提升', period: 3, maxPeriod: 3, phaseId: null, firstResourceId: '64c2bf2c-c23e-47f8-8dcf-bccc203174b6' }, { id: 'b2ec4202-c747-4ad2-a1b1-35a6c277c856', name: '科学素养提升', period: 1, maxPeriod: 1, phaseId: null, firstResourceId: '77b207ae-98cd-41e7-836c-afc8308871a6' }, { id: 'ca2a448a-5ff5-4183-92c2-6506f6696228', name: '心理健康教育能力提升', period: 1, maxPeriod: 1, phaseId: null, firstResourceId: 'a80fc623-af06-4b31-be65-28850719005c' }, // 选修课(7门,共3学时,按阶段汇总) { id: 'a09d7192-d7d6-4451-8828-a96d3a5e66ca', name: '学校美育浸润行动', period: 0, maxPeriod: -1, phaseId: ELECTIVE_PHASE_ID, firstResourceId: 'e3206646-a292-4009-ab79-6f753f9e7c69' }, { id: '6a46308e-17ff-456a-a320-b5bdb494e93d', name: '学科美育教学改革专题培训', period: 0, maxPeriod: -1, phaseId: ELECTIVE_PHASE_ID, firstResourceId: '87d178d5-5e65-4898-902d-7cec80479565' }, { id: 'f120b8f7-1768-440e-ae79-3e79dd0650f2', name: '有效开展每日综合体育活动', period: 0, maxPeriod: -1, phaseId: ELECTIVE_PHASE_ID, firstResourceId: 'b61073dd-5fb7-4e8f-b210-25ae7f73fdbb' }, { id: '0b69a573-7585-4450-8bae-48d3e3fd5cc1', name: '跨学科实验设计', period: 0, maxPeriod: -1, phaseId: ELECTIVE_PHASE_ID, firstResourceId: 'd8be0f8f-aedc-4301-bde3-578035e400d1' }, { id: '12a71d1f-8c22-418e-8ffe-ab0a6741fd74', name: '孤独症教育骨干教师培训', period: 0, maxPeriod: -1, phaseId: ELECTIVE_PHASE_ID, firstResourceId: '1784a0de-8870-4665-9b79-2c8c4dee8bc8' }, { id: 'c7044ffc-64b4-44b9-a866-0c117ff104d1', name: '融合教育通识课程', period: 0, maxPeriod: -1, phaseId: ELECTIVE_PHASE_ID, firstResourceId: '91284489-20bf-42a5-97a7-b19dc11ce068' }, { id: '87102185-fa21-4565-ad94-ade986c1c141', name: '面向普教校长与教师的融合教育通识课实录', period: 0, maxPeriod: -1, phaseId: ELECTIVE_PHASE_ID, firstResourceId: '9682ae4b-66ae-4185-8501-420547877a37' }, ]; // 为每门课程生成详情页URL COURSES.forEach(c => { c.detailUrl = `https://basic.smartedu.cn/teacherTraining/courseDetail?courseId=${c.id}&tag=${TAG_ENCODED}&channelId=&libraryId=${CONFIG.LIBRARY_ID}&breadcrumb=${TAG_ENCODED}&resourceId=${c.firstResourceId}`; }); // ==================== 工具函数 ==================== const Utils = { wait: (ms) => new Promise(r => setTimeout(r, ms)), log: (msg) => { const time = new Date().toLocaleTimeString(); console.log(`[${CONFIG.SCRIPT_NAME}] ${time} ${msg}`); }, // 生成随机字符串 randomStr: (len) => { const chars = '0123456789ABCDEFGHIJKLMNOPQRTUVWXZYS'; let res = ''; for (let i = 0; i < len; i++) { res += chars[Math.floor(Math.random() * chars.length)]; } return res; }, // 生成nonce generateNonce: (diff) => { return (Date.now() + parseInt(diff || 0, 10)) + ':' + Utils.randomStr(8); }, // 从localStorage获取动态token 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)) { const raw = localStorage.getItem(key); const parsed = JSON.parse(raw); const tokenObj = JSON.parse(parsed.value); return { key: key, appId: key.match(pattern)[1], token: tokenObj }; } } } catch (err) { Utils.log('获取Token失败: ' + err.message); } return null; }, // 获取userId getUserId: () => { try { const tokenInfo = Utils.getDynamicToken(); if (tokenInfo && tokenInfo.token && tokenInfo.token.user_id) { return tokenInfo.token.user_id; } } catch (_) {} return null; }, // 计算MAC签名 calcMac: (url, nonce, method, macKey) => { const u = new URL(url); const base = nonce + '\n' + method.toUpperCase() + '\n' + u.pathname + '\n' + u.hostname + '\n'; return CryptoJS.HmacSHA256(base, macKey).toString(CryptoJS.enc.Base64); }, // 生成MAC认证头 getMacAuthHeader: (url, method, tokenInfo) => { if (!tokenInfo || !tokenInfo.token) return null; const nonce = Utils.generateNonce(tokenInfo.token.diff); const mac = Utils.calcMac(url, nonce, method, tokenInfo.token.mac_key); return `MAC id="${tokenInfo.token.access_token}",nonce="${nonce}",mac="${mac}"`; }, // GM_xmlhttpRequest Promise封装 request: (options) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...options, onload: (res) => { if (res.status >= 200 && res.status < 300) { try { const data = JSON.parse(res.responseText); resolve({ data, res }); } catch { resolve({ data: res.responseText, res }); } } else { reject(new Error(`HTTP ${res.status}: ${res.responseText}`)); } }, onerror: (err) => reject(err), ontimeout: () => reject(new Error('请求超时')), }); }); }, }; // ==================== 欢迎弹窗 ==================== class WelcomeDialog { constructor() { this.shown = GM_getValue(CONFIG.WELCOME_SHOWN_KEY, false); this.dialog = null; this.floatingBall = null; this.fbDragState = { isDragging: false, startX: 0, startY: 0, offsetLeft: 0, offsetTop: 0, moved: false }; } shouldShow() { const p = location.pathname; // 每次回到首页都显示欢迎弹窗(不受 shown 标记限制) return p === '/' || p === '/index' || p === '/teacherTraining' || p === '/training'; } show() { if (this.dialog) return; const overlay = document.createElement('div'); overlay.id = 'sumuve-welcome-overlay'; overlay.style.cssText = ` position:fixed;top:0;left:0;width:100%;height:100%; background:rgba(0,0,0,0.5);z-index:2147483647; display:flex;align-items:center;justify-content:center; font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; `; const box = document.createElement('div'); box.style.cssText = ` background:#fff;border-radius:16px;padding:32px; box-shadow:0 20px 60px rgba(0,0,0,0.3); max-width:400px;width:90%;text-align:center;animation:sumuveFadeIn 0.3s ease; `; box.innerHTML = `

一键秒学助手

2026年基础教育暑期教师研修

脚本代开发请联系作者sumuve

`; overlay.appendChild(box); document.body.appendChild(overlay); this.dialog = overlay; // 绑定事件 box.querySelector('#sumuve-start-btn').addEventListener('click', () => this.onStartLearn()); box.querySelector('#sumuve-later-btn').addEventListener('click', () => this.onLater()); // ESC关闭 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.dialog) this.onLater(); }); // 添加动画 GM_addStyle(` @keyframes sumuveFadeIn { from { opacity:0; transform:scale(0.9); } to { opacity:1; transform:scale(1); } } `); } hide() { if (this.dialog) { this.dialog.remove(); this.dialog = null; } } async onStartLearn() { this.hide(); // 检测登录状态 const token = Utils.getDynamicToken(); if (!token) { this.showLoginPrompt(() => { // 登录提示关闭后,重新显示欢迎弹窗(用户仍未登录,可再次尝试或点稍后) this.show(); }); return; } // 触发主流程 MainController.getInstance().start(); } onLater() { this.hide(); if (this.fromComplete) { // 来自完成学时流程:显示绿色✓小圆球 showFinishBallAfterComplete(); } else { // 首页欢迎弹窗:显示蓝色▶小圆球 this.showFloatingBall(); } } // 显示可拖动小圆球 showFloatingBall() { if (this.floatingBall) return; const ball = document.createElement('div'); ball.id = 'sumuve-floating-ball'; const savedPos = this._restoreBallPosition(); const left = savedPos ? savedPos.left : Math.max(10, window.innerWidth - 80); const top = savedPos ? savedPos.top : Math.max(10, window.innerHeight / 2 - 30); ball.style.cssText = ` position:fixed;left:${left}px;top:${top}px; width:60px;height:60px;border-radius:50%; background:linear-gradient(135deg,#1e62ec,#3b82f6); box-shadow:0 6px 20px rgba(30,98,236,0.4); display:flex;align-items:center;justify-content:center; z-index:2147483646;cursor:pointer; user-select:none;transition:transform 0.15s ease,box-shadow 0.15s ease; animation:sumuveBallPop 0.3s ease; `; ball.innerHTML = ``; document.body.appendChild(ball); this.floatingBall = ball; this._bindBallEvents(ball); // 悬浮入场动画 GM_addStyle(` @keyframes sumuveBallPop { from { opacity:0; transform:scale(0.3); } to { opacity:1; transform:scale(1); } } `); } _bindBallEvents(ball) { const s = this.fbDragState; // 点击重新展开弹窗(注意区分拖拽和点击) ball.addEventListener('click', (e) => { if (s.moved) { s.moved = false; return; } this.hideFloatingBall(); this.show(); }); // 右键永久隐藏(作为应急出口) ball.addEventListener('contextmenu', (e) => { e.preventDefault(); this.hideFloatingBall(true); }); // 鼠标按下 — 开始拖拽 ball.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); const rect = ball.getBoundingClientRect(); s.isDragging = true; s.moved = false; s.startX = e.clientX; s.startY = e.clientY; s.offsetLeft = rect.left; s.offsetTop = rect.top; ball.style.transition = 'none'; ball.style.transform = 'scale(0.95)'; }); document.addEventListener('mousemove', (e) => { if (!s.isDragging) return; const dx = e.clientX - s.startX; const dy = e.clientY - s.startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) s.moved = true; let nx = s.offsetLeft + dx; let ny = s.offsetTop + dy; // 边界限制 nx = Math.max(0, Math.min(window.innerWidth - 60, nx)); ny = Math.max(0, Math.min(window.innerHeight - 60, ny)); ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { if (!s.isDragging) return; s.isDragging = false; ball.style.transform = ''; ball.style.transition = 'transform 0.15s ease,box-shadow 0.15s ease'; // 自动吸附到左右边缘 const rect = ball.getBoundingClientRect(); const cx = rect.left + rect.width / 2; let targetLeft = rect.left; if (cx < window.innerWidth / 2) targetLeft = 10; else targetLeft = window.innerWidth - 70; ball.style.transition = 'left 0.25s ease,transform 0.15s ease,box-shadow 0.15s ease'; ball.style.left = targetLeft + 'px'; ball.style.right = 'auto'; // 保存位置 setTimeout(() => this._saveBallPosition(ball), 300); }); // 触屏支持 ball.addEventListener('touchstart', (e) => { if (e.touches.length !== 1) return; const t = e.touches[0]; const rect = ball.getBoundingClientRect(); s.isDragging = true; s.moved = false; s.startX = t.clientX; s.startY = t.clientY; s.offsetLeft = rect.left; s.offsetTop = rect.top; ball.style.transition = 'none'; }, { passive: true }); document.addEventListener('touchmove', (e) => { if (!s.isDragging || e.touches.length !== 1) return; const t = e.touches[0]; const dx = t.clientX - s.startX; const dy = t.clientY - s.startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) s.moved = true; let nx = s.offsetLeft + dx; let ny = s.offsetTop + dy; nx = Math.max(0, Math.min(window.innerWidth - 60, nx)); ny = Math.max(0, Math.min(window.innerHeight - 60, ny)); ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }, { passive: true }); document.addEventListener('touchend', () => { if (!s.isDragging) return; s.isDragging = false; const rect = ball.getBoundingClientRect(); const cx = rect.left + rect.width / 2; let targetLeft = rect.left; if (cx < window.innerWidth / 2) targetLeft = 10; else targetLeft = window.innerWidth - 70; ball.style.transition = 'left 0.25s ease,transform 0.15s ease'; ball.style.left = targetLeft + 'px'; setTimeout(() => this._saveBallPosition(ball), 300); }); } hideFloatingBall(permanent = false) { if (this.floatingBall) { this.floatingBall.remove(); this.floatingBall = null; } if (permanent) { GM_deleteValue && GM_deleteValue('sumuve_fb_pos'); } } _saveBallPosition(ball) { try { const rect = ball.getBoundingClientRect(); GM_setValue('sumuve_fb_pos', JSON.stringify({ left: rect.left, top: rect.top })); } catch (_) {} } _restoreBallPosition() { try { const raw = GM_getValue('sumuve_fb_pos', null); if (!raw) return null; const p = JSON.parse(raw); return p.left != null ? p : null; } catch (_) { return null; } } showLoginPrompt(onClose) { const overlay = document.createElement('div'); overlay.style.cssText = ` position:fixed;top:0;left:0;width:100%;height:100%; background:rgba(0,0,0,0.5);z-index:2147483647; display:flex;align-items:center;justify-content:center; `; const box = document.createElement('div'); box.style.cssText = 'background:#fff;border-radius:16px;padding:28px;max-width:360px;width:90%;text-align:center;'; box.innerHTML = `
🔒

请先登录

您尚未登录智慧教育平台,请先登录后再使用一键秒学功能。

`; overlay.appendChild(box); document.body.appendChild(overlay); // 绑定关闭事件,关闭后触发回调 box.querySelector('#sumuve-login-ok').addEventListener('click', () => { overlay.remove(); if (typeof onClose === 'function') onClose(); }); } } // ==================== 进度面板 ==================== class ProgressPanel { constructor() { this.panel = null; // 从持久化存储恢复历史日志(最多100条) this.logLines = []; try { const raw = GM_getValue(CONFIG.LOGS_KEY, ''); if (raw) { const arr = JSON.parse(raw); if (Array.isArray(arr)) this.logLines = arr.slice(-100); } } catch (_) {} this.visible = true; } static getInstance() { if (!ProgressPanel._instance) { ProgressPanel._instance = new ProgressPanel(); } return ProgressPanel._instance; } create() { if (this.panel) return; const div = document.createElement('div'); div.id = 'sumuve-panel'; div.style.cssText = ` position:fixed;right:20px;bottom:20px;z-index:2147483646; width:360px;background:rgba(255,255,255,0.95);backdrop-filter:blur(10px); border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.15); font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; font-size:13px;color:#374151;overflow:hidden; border:1px solid rgba(0,0,0,0.05); `; div.innerHTML = `
⚡ 一键秒学助手 v1.0.1
×
📚 已学习学时 0.00 学时
✅ 已认定学时 0.00/10 学时
🎯 当前任务
等待开始...
--
📋 运行日志 📥 下载日志
`; document.body.appendChild(div); this.panel = div; // 绑定事件 div.querySelector('#sumuve-panel-close').addEventListener('click', () => this.hide()); // 关闭后显示小圆球 div.querySelector('#sumuve-pause-btn').addEventListener('click', () => { const mc = MainController.getInstance(); // 从持久化状态判断是否在运行(跨页面后 state.running 已重置) let flowRunning = false; try { const raw = GM_getValue(CONFIG.FLOW_STATE_KEY, ''); if (raw) flowRunning = !!JSON.parse(raw).running; } catch (_) {} if (!mc.state.running && !flowRunning) { // 未运行:启动秒学 mc.start(); } else { // 运行中:不重复启动 this.log('ℹ️ 学习正在进行中...'); } }); div.querySelector('#sumuve-stop-btn').addEventListener('click', () => { const mc = MainController.getInstance(); // 确保停止时 state 也同步 mc.state.running = true; mc.stop(); }); div.querySelector('#sumuve-download-log').addEventListener('click', () => this.downloadLog()); // 拖拽 this._makeDraggable(div.querySelector('#sumuve-panel-header'), div); // 恢复保存的位置 this._restorePosition(div); } _restorePosition(target) { try { const saved = GM_getValue(CONFIG.PANEL_POS_KEY, null); if (saved) { const pos = JSON.parse(saved); if (typeof pos.left === 'number' && typeof pos.top === 'number') { target.style.left = pos.left + 'px'; target.style.top = pos.top + 'px'; target.style.right = 'auto'; target.style.bottom = 'auto'; } } } catch (_) {} } _savePosition(target) { try { const rect = target.getBoundingClientRect(); GM_setValue(CONFIG.PANEL_POS_KEY, JSON.stringify({ left: rect.left, top: rect.top, })); } catch (_) {} } _makeDraggable(handle, target) { let isDragging = false, startX, startY, initLeft, initTop; handle.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = target.getBoundingClientRect(); initLeft = rect.left; initTop = rect.top; target.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; target.style.left = (initLeft + dx) + 'px'; target.style.top = (initTop + dy) + 'px'; target.style.right = 'auto'; target.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; target.style.transition = ''; // 拖拽结束后保存位置 this._savePosition(target); } }); } toggle() { const body = this.panel.querySelector('#sumuve-panel-body'); body.style.display = body.style.display === 'none' ? '' : 'none'; } show() { if (!this.panel) this.create(); this.panel.style.display = 'block'; // 如果有小圆球则隐藏 this._hideFloatBall(); } hide(showFloatBall = true) { if (this.panel) this.panel.style.display = 'none'; // 关闭面板时显示小圆球,方便重新打开(首页不需要) if (showFloatBall) this._showFloatBall(); } // 面板专用小圆球 _showFloatBall() { if (this._floatBall) return; const ball = document.createElement('div'); ball.id = 'sumuve-panel-floatball'; ball.style.cssText = ` position:fixed;right:20px;top:50%;transform:translateY(-50%); width:48px;height:48px;border-radius:50%; background:linear-gradient(135deg,#1e62ec,#3b82f6); box-shadow:0 4px 16px rgba(30,98,236,0.4); display:flex;align-items:center;justify-content:center; z-index:2147483646;cursor:pointer;user-select:none; transition:left 0.25s ease,top 0.25s ease; `; ball.innerHTML = ``; ball.title = '点击展开进度面板,拖动可移动位置'; // 拖拽 + 点击区分 let isDragging = false, moved = false, startX = 0, startY = 0, initLeft = 0, initTop = 0; ball.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); const rect = ball.getBoundingClientRect(); isDragging = true; moved = false; startX = e.clientX; startY = e.clientY; initLeft = rect.left; initTop = rect.top; ball.style.transition = 'none'; ball.style.transform = 'scale(0.92)'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; let nx = Math.max(0, Math.min(window.innerWidth - 48, initLeft + dx)); let ny = Math.max(0, Math.min(window.innerHeight - 48, initTop + dy)); ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; }); document.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; ball.style.transform = ''; ball.style.transition = 'left 0.25s ease,top 0.25s ease'; // 点击(未拖动)则展开面板 if (!moved) { this.show(); return; } // 拖动结束:吸附到最近边缘 const rect = ball.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const targetLeft = cx < window.innerWidth / 2 ? 10 : window.innerWidth - 58; ball.style.left = targetLeft + 'px'; }); // 触屏支持 ball.addEventListener('touchstart', (e) => { if (e.touches.length !== 1) return; const t = e.touches[0]; const rect = ball.getBoundingClientRect(); isDragging = true; moved = false; startX = t.clientX; startY = t.clientY; initLeft = rect.left; initTop = rect.top; ball.style.transition = 'none'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }, { passive: true }); document.addEventListener('touchmove', (e) => { if (!isDragging || e.touches.length !== 1) return; const t = e.touches[0]; const dx = t.clientX - startX; const dy = t.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; let nx = Math.max(0, Math.min(window.innerWidth - 48, initLeft + dx)); let ny = Math.max(0, Math.min(window.innerHeight - 48, initTop + dy)); ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; }, { passive: true }); document.addEventListener('touchend', () => { if (!isDragging) return; isDragging = false; ball.style.transition = 'left 0.25s ease,top 0.25s ease'; if (!moved) { this.show(); return; } const rect = ball.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const targetLeft = cx < window.innerWidth / 2 ? 10 : window.innerWidth - 58; ball.style.left = targetLeft + 'px'; }); document.body.appendChild(ball); this._floatBall = ball; } _hideFloatBall() { if (this._floatBall) { this._floatBall.remove(); this._floatBall = null; } } updatePeriod(earned, target) { // 兼容旧调用:earned=已学习,target=目标(默认10) this.updateEarned(earned, target); } updateEarned(earned, target) { // 已学习学时不限定10,进度条自适应:未达10按10为分母,超过10则满格 const denom = Math.max(earned, target || CONFIG.TARGET_PERIOD); const pct = denom > 0 ? Math.min(100, (earned / denom) * 100) : 0; const text = document.getElementById('sumuve-earned-text'); const bar = document.getElementById('sumuve-earned-bar'); if (text) text.textContent = `${earned.toFixed(2)} 学时`; if (bar) bar.style.width = pct + '%'; } updateCertified(cert, target) { target = target || CONFIG.TARGET_PERIOD; const pct = target > 0 ? Math.min(100, (cert / target) * 100) : 0; const text = document.getElementById('sumuve-cert-text'); const bar = document.getElementById('sumuve-cert-bar'); if (text) text.textContent = `${cert.toFixed(2)}/${target} 学时`; if (bar) bar.style.width = pct + '%'; } updateVideo(done, total) { const pct = total > 0 ? Math.min(100, (done / total) * 100) : 0; const text = document.getElementById('sumuve-video-text'); const bar = document.getElementById('sumuve-video-bar'); if (text) text.textContent = `${done}/${total} 个`; if (bar) bar.style.width = pct + '%'; } updateTask(name, mode) { const taskEl = document.getElementById('sumuve-current-task'); const modeEl = document.getElementById('sumuve-current-mode'); if (taskEl) taskEl.textContent = name || '等待开始...'; if (modeEl) modeEl.textContent = mode || '--'; } log(message) { Utils.log(message); this.logLines.push(`${new Date().toLocaleTimeString()} ${message}`); if (this.logLines.length > 100) this.logLines = this.logLines.slice(-100); // 持久化到GM存储,刷新不丢失 try { GM_setValue(CONFIG.LOGS_KEY, JSON.stringify(this.logLines)); } catch (_) {} const logsEl = document.getElementById('sumuve-logs'); if (logsEl) { logsEl.innerHTML = this.logLines.map(l => `
${l}
`).join(''); logsEl.scrollTop = logsEl.scrollHeight; } } // 下载日志为Markdown文件 downloadLog() { const now = new Date(); const ts = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + '_' + String(d.getHours()).padStart(2, '0') + '-' + String(d.getMinutes()).padStart(2, '0') + '-' + String(d.getSeconds()).padStart(2, '0'); let md = `# 一键秒学助手 运行日志\n\n`; md += `**导出时间**: ${now.toLocaleString('zh-CN')}\n\n`; md += `---\n\n`; md += `### 日志内容\n\n`; md += `\`\`\`\n`; this.logLines.forEach(line => { md += line + '\n'; }); md += `\`\`\`\n`; const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `秒学助手日志_${ts(now)}.md`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.log('📥 日志已导出为Markdown文件'); } updatePauseButton(state) { const btn = document.getElementById('sumuve-pause-btn'); if (!btn) return; if (state === 'idle') { btn.textContent = '开始'; btn.style.background = '#ecfdf5'; btn.style.color = '#047857'; btn.disabled = false; btn.style.cursor = 'pointer'; btn.style.opacity = '1'; } else if (state === 'running') { btn.textContent = '运行中'; btn.style.background = '#f3f4f6'; btn.style.color = '#9ca3af'; btn.disabled = true; btn.style.cursor = 'not-allowed'; btn.style.opacity = '0.7'; } else if (state === 'paused') { // 兼容残留调用,不再使用暂停状态 btn.textContent = '开始'; btn.style.background = '#ecfdf5'; btn.style.color = '#047857'; btn.disabled = false; btn.style.cursor = 'pointer'; btn.style.opacity = '1'; } } } // ==================== 秒学模块 ==================== class SecondLearning { constructor() { this.tokenInfo = null; } init() { this.tokenInfo = Utils.getDynamicToken(); if (!this.tokenInfo) throw new Error('未获取到登录凭证'); } // 上报视频位置(秒学核心) async reportVideoPosition(resourceId, userId, duration) { const url = `https://x-study-record-api.ykt.eduyun.cn/v1/resource_learning_positions/${resourceId}/${userId}`; const auth = Utils.getMacAuthHeader(url, 'PUT', this.tokenInfo); if (!auth) throw new Error('MAC认证失败'); const result = await Utils.request({ url: url, method: 'PUT', headers: { 'authorization': auth, 'content-type': 'application/json', 'sdp-app-id': this.tokenInfo.appId, }, data: JSON.stringify({ position: Math.max(0, duration - 3) }), }); return result; } // 上报课程进度 async reportCourseProgress(trainId, courseId, progressData) { const url = `https://elearning-train-gateway.ykt.eduyun.cn/v1/spi/trains/${trainId}/courses/${courseId}/progress/actions/async`; const auth = Utils.getMacAuthHeader(url, 'POST', this.tokenInfo); if (!auth) throw new Error('MAC认证失败'); const result = await Utils.request({ url: url, method: 'POST', headers: { 'authorization': auth, 'content-type': 'application/json', 'sdp-app-id': this.tokenInfo.appId, }, data: JSON.stringify(progressData), }); return result; } // 查询已获学时(使用 courses_period API,最可靠的数据源) // 注意:选修课的学时需要从阶段ID(phaseId)读取,避免重复计算 async queryEarnedPeriod(userId, trainId) { try { const url = `https://elearning-train-api.ykt.eduyun.cn/v1/users/${userId}/trains/${trainId}/courses_period/actions/list`; const auth = Utils.getMacAuthHeader(url, 'GET', this.tokenInfo); if (auth) { const result = await Utils.request({ url: url, method: 'GET', headers: { 'authorization': auth, 'sdp-app-id': this.tokenInfo.appId, }, }); if (result && result.data) { const periodData = result.data; // 已学习学时计算: // 1. 累加必修课的已学习学时 // 2. 加上选修课阶段(phaseId)的学时 let totalEarned = 0; for (const course of COURSES) { if (course.maxPeriod > 0) { // 必修课:直接累加已学习学时 const earned = Number(periodData[course.id]) || 0; if (earned > 0) totalEarned += earned; } } // 选修课:使用阶段学时(避免与单课程重复) const phaseEarned = Number(periodData[ELECTIVE_PHASE_ID]) || 0; if (phaseEarned > 0) totalEarned += phaseEarned; periodData._totalEarned = totalEarned; // 已认定学时:通过 _calculateCertifiedFromPeriodData 计算 periodData._certified = this._calculateCertifiedFromPeriodData(periodData); return periodData; } } } catch (_) {} return { _totalEarned: 0, _certified: 0 }; } // 从 courses_period 数据中计算已认定学时 // 判断标准: // 1. 必修课:已学习学时 >= maxPeriod(认定要求) // 2. 选修课:按阶段汇总,读取 phaseId 对应的学时 _calculateCertifiedFromPeriodData(periodData) { let certified = 0; let requiredCertified = 0; let electiveCertified = 0; // 必修课:逐课程判断 for (const course of COURSES) { if (course.maxPeriod > 0) { // 必修课:已学习 >= maxPeriod 视为已认定 const earned = Number(periodData[course.id]) || 0; const status = Number(periodData[course.id + '-status']) || 0; if (earned >= course.maxPeriod || status >= 1) { certified += course.period; // 使用配置的认定学时 requiredCertified += course.period; } } } // 选修课:按阶段汇总认定(phaseId 对应的学时) // 从 API 返回的数据中读取阶段学时 const phaseEarned = Number(periodData[ELECTIVE_PHASE_ID]) || 0; const phaseStatus = Number(periodData[ELECTIVE_PHASE_ID + '-status']) || 0; if (phaseEarned > 0 || phaseStatus >= 1) { // 选修课认定学时 = 阶段已学习学时(最多3学时) electiveCertified = Math.min(phaseEarned, 3); certified += electiveCertified; } // 调试日志:输出详细的学时计算结果 Utils.log(`[学时计算] 必修认定:${requiredCertified.toFixed(2)} 选修认定:${electiveCertified.toFixed(2)} 总认定:${certified.toFixed(2)} 选修已学习:${phaseEarned.toFixed(2)}`); return certified; } // res_stats 接口返回的是页面访问统计数据(PV/UV),不适用于学时计算 // 保留此方法用于其他可能的扩展场景 async _queryCertifiedFromResStats() { // 该 API 返回的是课程的 PV/UV 等统计数据,不包含学时信息 // 学时计算应使用 courses_period API 或 DOM 抓取 return null; } // 获取课程资源列表 async getCourseResources(courseId) { try { // 先获取课程详情,拿到activity_set_id const infoUrl = `https://s-file-1.ykt.cbern.com.cn/teach/s_course/v2/business_courses/${courseId}/course_relative_infos/zh-CN.json`; const infoRes = await fetch(infoUrl, { credentials: 'omit' }); const infoData = await infoRes.json(); const activitySetId = infoData?.course_detail?.activity_set_id; if (!activitySetId) throw new Error('未找到activity_set_id'); // 获取资源树 const fullsUrl = `https://s-file-2.ykt.cbern.com.cn/teach/s_course/v2/activity_sets/${activitySetId}/fulls.json`; const fullsRes = await fetch(fullsUrl, { credentials: 'omit' }); const fullsData = await fullsRes.json(); const resources = []; const walk = (nodes) => { nodes.forEach(node => { if (node.node_type === 'catalog' && node.child_nodes) { walk(node.child_nodes); } else if (node.node_type === 'activity') { const activity = node.relations?.activity; if (activity && activity.activity_resources) { activity.activity_resources.forEach(res => { if (res.resource_type === 'video') { resources.push({ resourceId: res.resource_id, title: node.node_name || '', duration: Number(res.study_time || res.video_extend?.duration || 0), }); } }); } } }); }; if (fullsData.nodes) walk(fullsData.nodes); return resources; } catch (err) { Utils.log('获取课程资源失败: ' + err.message); return []; } } } // ==================== 自动学习模块 ==================== class AutoLearning { constructor() { this.video = null; this.watchdog = null; this.onEndedCallback = null; } start(videoElement, onEnded, skipToEnd = false) { // 如果是同一个 video,先清理旧的事件监听器 if (this.video && this.video !== videoElement) { // 旧 video 会被 GC 回收,无需手动 removeEventListener } this.video = videoElement; this.onEndedCallback = onEnded; // 记录自动播放开始时间,用于确保至少播放4秒后才允许判定结束 this._playStartTime = Date.now(); this._skipDone = false; this._configureVideo(skipToEnd); this._startWatchdog(); this._bindVisibilityHandler(); // 使用命名函数以便后续清理 if (!this._endedHandler) { this._endedHandler = () => this._handleEnded(); this._pauseHandler = () => this._handlePause(); } this.video.addEventListener('ended', this._endedHandler); this.video.addEventListener('pause', this._pauseHandler); } stop() { if (this.watchdog) { clearInterval(this.watchdog); this.watchdog = null; } if (this._visibilityHandler) { document.removeEventListener('visibilitychange', this._visibilityHandler); this._visibilityHandler = null; } // 清理播放状态 this._playStartTime = null; // 移除旧 video 的事件监听器 if (this.video && this._endedHandler) { this.video.removeEventListener('ended', this._endedHandler); this.video.removeEventListener('pause', this._pauseHandler); } // 真正暂停页面上的所有 video 元素(不只清引用) if (this.video) { try { this.video.pause(); } catch (_) {} this.video = null; } document.querySelectorAll('video').forEach(v => { try { v.pause(); } catch (_) {} }); } _configureVideo(skipToEnd = false) { if (!this.video) return; this.video.muted = true; this.video.playbackRate = 2; // 如果视频已结束(ended=true),先重置currentTime来解除结束状态 if (this.video.ended) { try { this.video.currentTime = 0; } catch (_) {} } // 跳转至视频最后5秒(确保 duration 已就绪) const doSkip = () => { const dur = Number(this.video.duration || 0); if (skipToEnd && dur > 5 && isFinite(dur)) { this.video.currentTime = Math.max(0, dur - 5); } }; // duration 可能还没加载,监听 loadedmetadata if (this.video.readyState >= 1 && Number(this.video.duration || 0) > 0) { doSkip(); } else { this.video.addEventListener('loadedmetadata', doSkip, { once: true }); // 兜底:1秒后再试一次 setTimeout(doSkip, 1000); } if (this.video.paused) { this.video.play().catch(() => {}); } } _startWatchdog() { // 先清除旧的 watchdog(防止重复创建) if (this.watchdog) { clearInterval(this.watchdog); this.watchdog = null; } this.watchdog = setInterval(() => { if (!this.video) return; // 暂停或停止状态:强制 pause video,并不再恢复播放 const paused = GM_getValue(CONFIG.PAUSED_KEY, '') === '1'; const stopped = GM_getValue(CONFIG.STOPPED_KEY, '') === '1'; if (paused || stopped) { try { this.video.pause(); } catch (_) {} this._dismissModals(); return; } // 保持静音和倍速 this.video.muted = true; this.video.playbackRate = 2; if (this.video.paused && !this.video.ended) { this.video.play().catch(() => {}); } // 确保跳转到最后5秒(平台可能重置进度) const dur = Number(this.video.duration || 0); const cur = Number(this.video.currentTime || 0); if (dur > 5 && isFinite(dur) && cur < dur - 5) { this.video.currentTime = Math.max(0, dur - 5); } // 检测视频结束:必须满足以下任一条件 // 1. video.ended 事件触发(最可靠) // 2. 已播放至少4秒 且 当前时间已接近末尾(cur >= dur - 0.5) // 注意:不能用 cur/dur >= 0.98,因为跳到最后5秒时比值已超过0.98 const elapsed = Date.now() - (this._playStartTime || 0); const playedEnough = elapsed >= 4000; // 至少播放4秒 if (dur > 0 && this.video.ended) { this._handleEnded(); } else if (dur > 0 && playedEnough && cur >= dur - 0.5) { // 已播放足够时间且接近末尾,视为完成 this._handleEnded(); } // 关闭弹窗 this._dismissModals(); }, 2000); } _handlePause() { if (!this.video || this.video.ended) return; // 如果已停止,不自动恢复播放 if (GM_getValue(CONFIG.STOPPED_KEY, '') === '1') return; setTimeout(() => { if (GM_getValue(CONFIG.STOPPED_KEY, '') === '1') return; if (this.video && this.video.paused && !this.video.ended) { this.video.play().catch(() => {}); } }, 500); } _handleEnded() { if (this.onEndedCallback) { this.onEndedCallback(); this.onEndedCallback = null; // 防止重复触发 } } _bindVisibilityHandler() { this._visibilityHandler = () => { if (!this.video) return; if (document.visibilityState === 'visible' && this.video.paused && !this.video.ended) { this.video.play().catch(() => {}); } }; document.addEventListener('visibilitychange', this._visibilityHandler); } _dismissModals() { const selectors = [ '.ant-modal-wrap','.el-dialog__wrapper','.swal2-container', '[class*="modal"][class*="show"]','[class*="dialog"][class*="open"]' ]; selectors.forEach(sel => { document.querySelectorAll(sel).forEach(el => { const closeBtn = el.querySelector('.ant-modal-close,.el-dialog__close,.swal2-close,[class*="close"]'); if (closeBtn) closeBtn.click(); }); }); } } // ==================== 主控制器 ==================== class MainController { constructor() { this.state = { running: false, paused: false, courses: [], currentCourseIndex: -1, currentVideoIndex: -1, doneVideos: 0, totalVideos: 0, earnedPeriod: 0, targetPeriod: CONFIG.TARGET_PERIOD, mode: 'idle', // idle / second-learning / auto-learning }; this.secondLearning = new SecondLearning(); this.autoLearning = new AutoLearning(); this.panel = ProgressPanel.getInstance(); } static getInstance() { if (!MainController._instance) { MainController._instance = new MainController(); } return MainController._instance; } // ========== 跨页面流程状态机 ========== _loadFlowState() { try { const raw = GM_getValue(CONFIG.FLOW_STATE_KEY, ''); if (raw) { const f = JSON.parse(raw); // 兼容旧版状态:补全新字段 if (!f.processedCourseIds) f.processedCourseIds = {}; if (!f.processedResourceIds) f.processedResourceIds = {}; if (f.secondLearnDone) delete f.secondLearnDone; // 已废弃 if (f.autoLearnIdx === undefined) f.autoLearnIdx = -1; // -1=秒学阶段 if (f.certifiedPeriod === undefined) f.certifiedPeriod = 0; if (!f.startTime) f.startTime = Date.now(); // 记录开始时间 return f; } } catch (_) {} return { running: false, courseIdx: 0, videoIdx: 0, doneVideos: 0, totalVideos: 0, earnedPeriod: 0, certifiedPeriod: 0, processedCourseIds: {}, processedResourceIds: {}, autoLearnIdx: -1, // -1=秒学阶段, >=0=自动学习阶段 startTime: Date.now() // 记录开始时间 }; } _saveFlowState(s) { GM_setValue(CONFIG.FLOW_STATE_KEY, JSON.stringify(s)); } _clearFlowState() { GM_setValue(CONFIG.FLOW_STATE_KEY, ''); GM_setValue(CONFIG.PERIOD_ASKED_KEY, ''); } // 构建课程详情页URL(指定video resourceId) _buildDetailUrl(course, resourceId) { const rid = resourceId || course.firstResourceId; return `https://basic.smartedu.cn/teacherTraining/courseDetail?courseId=${course.id}&tag=${encodeURIComponent(CONFIG.TAG)}&channelId=&libraryId=${CONFIG.LIBRARY_ID}&breadcrumb=${encodeURIComponent(CONFIG.TAG)}&resourceId=${rid}`; } async start() { if (this.state.running) return; // 清除停止标记,启动新的一次运行 GM_setValue(CONFIG.STOPPED_KEY, ''); this.state.running = true; this.panel.show(); // 清除暂停残留(不再使用暂停功能) GM_setValue(CONFIG.PAUSED_KEY, ''); this.state.paused = false; this.panel.updatePauseButton('running'); this.panel.log('🚀 一键秒学已启动'); try { this.secondLearning.init(); const userId = Utils.getUserId(); if (!userId) throw new Error('无法获取用户ID,请先登录'); this.state.userId = userId; // 初始化流程状态 const flow = this._loadFlowState(); // 检测是否有未完成的断点(即使 flow.running=false) const hasProgress = flow.courseIdx > 0 || flow.autoLearnIdx >= 0 || Object.keys(flow.processedCourseIds || {}).length > 0; if (!hasProgress) { // 首次启动 → 重置流程状态 flow.running = true; flow.courseIdx = 0; flow.videoIdx = 0; flow.doneVideos = 0; flow.totalVideos = 0; flow.processedCourseIds = {}; flow.processedResourceIds = {}; flow.autoLearnIdx = -1; // -1=秒学阶段 flow.startTime = Date.now(); // 记录开始时间 // 预估总视频数 let total = 0; for (const c of COURSES) { try { const rs = await this.secondLearning.getCourseResources(c.id); total += rs.length; } catch (_) {} } flow.totalVideos = total; this._saveFlowState(flow); this.state.totalVideos = total; this.panel.log('🆕 首次启动,从头开始学习'); } else { // 有断点 → 从断点继续 flow.running = true; this._saveFlowState(flow); this.state.totalVideos = flow.totalVideos || 0; this.state.doneVideos = flow.doneVideos || 0; this.panel.log(`📍 从断点继续:课程[${flow.courseIdx + 1}/${COURSES.length}],视频索引=${flow.autoLearnIdx}`); } // 初始化学时显示 try { const pd = await this.secondLearning.queryEarnedPeriod(userId, CONFIG.TRAIN_ID); this.state.earnedPeriod = pd._totalEarned || 0; this.state.certifiedPeriod = pd._certified || 0; this.panel.updateEarned(this.state.earnedPeriod, CONFIG.TARGET_PERIOD); this.panel.updateCertified(this.state.certifiedPeriod, CONFIG.TARGET_PERIOD); this.panel.log(`💡 已学习 ${this.state.earnedPeriod.toFixed(2)} 学时 | 已认定 ${this.state.certifiedPeriod.toFixed(2)} / 目标 ${CONFIG.TARGET_PERIOD} 学时`); } catch (_) {} // 判断当前页面:如果不是课程详情页,就跳转到第一个未完成课程的详情页 const path = location.pathname; if (path.includes('/teacherTraining/courseDetail')) { // 在课程详情页,执行当前页视频处理流程 await this._processCurrentDetailPage(); } else { // 非详情页,跳转到第一个未完成课程 await this._jumpToNextPendingCourse(); } } catch (err) { this.panel.log('❌ 错误: ' + err.message); console.error(err); } } // 找到下一个未完成课程并跳转(必修4门优先 → 选修 → 总认定达10停止) async _jumpToNextPendingCourse() { if (!this._checkContinue()) return; const flow = this._loadFlowState(); const userId = this.state.userId || Utils.getUserId(); let periodData = null; try { periodData = await this.secondLearning.queryEarnedPeriod(userId, CONFIG.TRAIN_ID); this.state.earnedPeriod = periodData._totalEarned || 0; this.state.certifiedPeriod = periodData._certified || 0; this.panel.updateEarned(this.state.earnedPeriod, CONFIG.TARGET_PERIOD); this.panel.updateCertified(this.state.certifiedPeriod, CONFIG.TARGET_PERIOD); this.panel.log(`📊 当前已学习 ${this.state.earnedPeriod.toFixed(2)} 学时 | 已认定 ${this.state.certifiedPeriod.toFixed(2)}/${CONFIG.TARGET_PERIOD} 学时`); } catch (_) {} this.panel.log('━━━ 开始检查课程完成情况 ━━━'); for (let i = 0; i < COURSES.length; i++) { const c = COURSES[i]; const status = periodData ? Number(periodData[`${c.id}-status`] || 0) : 0; const earned = periodData ? Number(periodData[c.id] || 0) : 0; const required = c.period || 0.01; const isCertDone = (status >= 1) || (earned >= required - 1e-6); const courseType = c.period > 0 ? `必修(认定${c.period}学时)` : '选修'; const earnedStr = earned.toFixed(2); const reqStr = c.period > 0 ? c.period.toFixed(2) : '—'; if (isCertDone) { this.panel.log(`✅ 课程[${i + 1}/${COURSES.length}] ${c.name} (${courseType}) 已达标(已学习${earnedStr}/认定要求${reqStr}),跳过`); continue; } // 找到未达标课程 flow.courseIdx = i; flow.videoIdx = 0; flow.autoLearnIdx = -1; // -1 = 从秒学阶段开始(强制秒学) this._saveFlowState(flow); const url = this._buildDetailUrl(c); this.panel.log(`➡️ 课程[${i + 1}/${COURSES.length}] ${c.name} (${courseType}) 未达标(已学习${earnedStr}/认定要求${reqStr}),开始学习`); this.panel.updateTask(c.name, '跳转课程详情页...'); GM_setValue(CONFIG.AUTO_START_KEY, '1'); setTimeout(() => { window.location.href = url; }, 600); return; } this.panel.log('━━━ 所有课程已检查完毕 ━━━'); await this._checkComplete(true); } // 在课程详情页时的处理流程: // 阶段1:先秒学整个课程的所有视频(API上报,不跳转) // 阶段2:再逐个跳转视频页面,自动播放最后5秒 async _processCurrentDetailPage() { if (!this._checkContinue()) return; const flow = this._loadFlowState(); if (!flow.running) return; const urlParams = new URLSearchParams(location.search); const courseId = urlParams.get('courseId'); const resourceId = urlParams.get('resourceId'); const course = COURSES.find(c => c.id === courseId); if (!course) { this.panel.log('⚠️ 未找到课程信息'); return; } flow.courseIdx = COURSES.findIndex(c => c.id === courseId); const courseType = course.period > 0 ? `必修(认定${course.period}学时)` : '选修'; this.panel.log(`━━━ 进入课程[${flow.courseIdx + 1}/${COURSES.length}]: ${course.name} (${courseType}) ━━━`); this.panel.updateTask(course.name, '加载视频资源...'); // 获取课程所有视频资源 let resources; try { resources = await this.secondLearning.getCourseResources(course.id); } catch (err) { this.panel.log('❌ 获取课程资源失败: ' + err.message); return; } if (!resources.length) { this.panel.log('⚠️ 课程无视频资源,跳到下一课'); flow.processedCourseIds[course.id] = true; flow.courseIdx++; this._saveFlowState(flow); await this._maybeAskPeriod(); await this._jumpToNextPendingCourse(); return; } this.panel.log(`📋 课程共 ${resources.length} 个视频`); // autoLearnIdx: -1 = 秒学阶段, >=0 = 自动学习阶段(对应视频索引) if (flow.autoLearnIdx === undefined || flow.autoLearnIdx === null) flow.autoLearnIdx = -1; const isInSecondLearnPhase = (flow.autoLearnIdx === -1); // ========== 阶段1:秒学所有视频(强制执行,不跳过)========== if (isInSecondLearnPhase) { this.panel.log(`⏩ 阶段1:开始秒学全部 ${resources.length} 个视频(强制执行)`); for (let i = 0; i < resources.length; i++) { if (!this._checkContinue()) return; // 暂停检查:暂停时阻塞等待,恢复后继续(停止则中断) if (!await this._waitIfPaused()) return; if (!this._checkContinue()) return; const res = resources[i]; const title = res.title || `视频${i + 1}`; const already = !!flow.processedResourceIds[res.resourceId]; try { this.panel.updateTask(`${course.name}`, `秒学中 [${i + 1}/${resources.length}] ${title}`); if (already) { this.panel.log(` [${i + 1}/${resources.length}] 秒学中: ${title}(之前已秒学过,重新执行)`); } else { this.panel.log(` [${i + 1}/${resources.length}] 秒学中: ${title}`); } await this.secondLearning.reportVideoPosition(res.resourceId, this.state.userId, res.duration || 300); flow.processedResourceIds[res.resourceId] = true; if (!already) { flow.doneVideos = (flow.doneVideos || 0) + 1; this.state.doneVideos = flow.doneVideos; } this.panel.log(` [${i + 1}/${resources.length}] ✅ 秒学成功: ${title}`); await Utils.wait(300); } catch (err) { this.panel.log(` [${i + 1}/${resources.length}] ❌ 秒学失败: ${title} - ${err.message}`); } } // 秒学阶段完成 → 切换到自动学习阶段 flow.autoLearnIdx = 0; this._saveFlowState(flow); this.panel.log(`✅ 阶段1完成:课程「${course.name}」全部视频秒学完毕`); // 秒学完刷新一次学时 await this._refreshPeriod(); // 检查是否已停止 if (!this._checkContinue()) return; } else { this.panel.log(`⏩ 阶段1已完成(秒学阶段已跳过),直接进入自动学习`); } // ========== 阶段2:自动学习(逐个视频播放最后5秒)========== if (flow.autoLearnIdx < 0) flow.autoLearnIdx = 0; this.panel.log(`▶️ 阶段2:开始自动学习(播放最后5秒确保认定)`); // 确定当前应该学习的视频索引 let targetIdx = flow.autoLearnIdx; if (targetIdx >= resources.length) { // 所有视频自动学习完毕 this._onCourseAutoLearnDone(course, flow); return; } const targetRes = resources[targetIdx]; const targetTitle = targetRes.title || `视频${targetIdx + 1}`; // 如果当前页面不是目标视频,跳转过去 if (resourceId !== targetRes.resourceId) { this.panel.log(` [${targetIdx + 1}/${resources.length}] 跳转至视频: ${targetTitle}`); this.panel.updateTask(course.name, `加载视频 [${targetIdx + 1}/${resources.length}]`); const jumpUrl = this._buildDetailUrl(course, targetRes.resourceId); GM_setValue(CONFIG.AUTO_START_KEY, '1'); this._saveFlowState(flow); setTimeout(() => { window.location.href = jumpUrl; }, 500); return; } // 当前页面正好是目标视频 → 暂停检查后自动播放最后5秒 if (!this._checkContinue()) return; if (!await this._waitIfPaused()) return; if (!this._checkContinue()) return; this.panel.log(` [${targetIdx + 1}/${resources.length}] 自动学习中: ${targetTitle}(播放最后5秒)`); this.panel.updateTask(course.name, `自动学习 [${targetIdx + 1}/${resources.length}] ${targetTitle}`); this.state.mode = 'auto-learning'; try { await this._autoPlayCurrentVideo(); } catch (err) { this.panel.log(` [${targetIdx + 1}/${resources.length}] ⚠️ 自动播放异常: ${err.message}`); } this.state.mode = 'idle'; // 关键检查:如果已停止,不要继续执行后续流程 if (!this._checkContinue()) return; this.panel.log(` [${targetIdx + 1}/${resources.length}] ✅ 自动学习完成: ${targetTitle}`); // 刷新学时 await this._refreshPeriod(); // 检查是否已停止(关键:防止停止后继续跳转下一个视频) if (!this._checkContinue()) return; // 检查认定学时是否达标 const shouldContinue = await this._maybeAskPeriod(); if (shouldContinue === false) return; // 移动到下一个视频 flow.autoLearnIdx = targetIdx + 1; this._saveFlowState(flow); if (flow.autoLearnIdx >= resources.length) { // 所有视频自动学习完毕 this._onCourseAutoLearnDone(course, flow); } else { // 跳转到下一个视频 const nextRes = resources[flow.autoLearnIdx]; const nextTitle = nextRes.title || `视频${flow.autoLearnIdx + 1}`; this.panel.log(` ➡️ 下一个视频: ${nextTitle}`); this.panel.updateTask(course.name, `加载视频 [${flow.autoLearnIdx + 1}/${resources.length}]`); const jumpUrl = this._buildDetailUrl(course, nextRes.resourceId); GM_setValue(CONFIG.AUTO_START_KEY, '1'); this._saveFlowState(flow); setTimeout(() => { window.location.href = jumpUrl; }, 500); } } // 课程所有视频自动学习完毕 async _onCourseAutoLearnDone(course, flow) { if (!this._checkContinue()) return; // 上报课程进度 try { await this._reportCourseProgress(course); } catch (_) {} flow.processedCourseIds[course.id] = true; flow.courseIdx++; flow.autoLearnIdx = -1; // 下一课从秒学阶段开始(强制秒学) this._saveFlowState(flow); this.panel.log(`✅ 课程「${course.name}」全部学习完成`); // 刷新学时 await this._refreshPeriod(); this.panel.log(`📊 当前已学习 ${this.state.earnedPeriod.toFixed(2)} 学时 | 已认定 ${this.state.certifiedPeriod.toFixed(2)}/${CONFIG.TARGET_PERIOD} 学时`); // 检查认定学时是否达标 const shouldContinue = await this._maybeAskPeriod(); if (shouldContinue === false) return; // 跳到下一课 this.panel.log(`➡️ 进入下一个课程...`); await this._jumpToNextPendingCourse(); } // 等待当前页面 video 出现并播放最后5秒 _autoPlayCurrentVideo() { return new Promise((resolve) => { // 保存 resolve 引用,以便 stop() 时能强制终止 this._autoPlayResolve = resolve; const totalTimeout = setTimeout(() => { this.panel.log('⏱️ 自动播放超时(25秒),继续下一步'); this._clearAutoPlayTimers(); this._autoPlayResolve = null; resolve(); }, 25000); this._autoPlayTimers = { totalTimeout }; // 等 video 元素出现 let tries = 0; const waitVideo = setInterval(() => { // 检查停止状态 if (GM_getValue(CONFIG.STOPPED_KEY, '') === '1') { clearInterval(waitVideo); clearTimeout(totalTimeout); this._clearAutoPlayTimers(); this._autoPlayResolve = null; resolve(); return; } tries++; const video = document.querySelector('video'); if (video && video.src) { clearInterval(waitVideo); this.panel.log('▶️ 开始自动播放视频(跳至最后5秒,静音2倍速)'); // 记录 play start time 用于确保至少播放4秒 this.autoLearning._playStartTime = Date.now(); this.autoLearning._skipDone = false; // 启动自动学习(带回调) this.autoLearning.start(video, () => { this._clearAutoPlayTimers(); this.autoLearning.stop(); this.panel.log('⏹️ 视频播放完毕'); clearTimeout(totalTimeout); this._autoPlayResolve = null; setTimeout(resolve, 1500); }, true); return; } if (tries > 40) { // 约20秒没等到video clearInterval(waitVideo); this.panel.log('ℹ️ 未检测到video元素,跳过自动播放'); clearTimeout(totalTimeout); this._clearAutoPlayTimers(); this._autoPlayResolve = null; setTimeout(resolve, 1500); } }, 500); }); } _clearAutoPlayTimers() { if (this._autoPlayTimers) { if (this._autoPlayTimers.totalTimeout) clearTimeout(this._autoPlayTimers.totalTimeout); if (this._autoPlayTimers.watchdogTimer) clearTimeout(this._autoPlayTimers.watchdogTimer); this._autoPlayTimers = null; } // 如果 _autoPlayCurrentVideo 还在等待,强制 resolve if (this._autoPlayResolve) { const resolve = this._autoPlayResolve; this._autoPlayResolve = null; resolve(); } } async _refreshPeriod() { try { // 优先从页面DOM抓取(最准确,与首页显示同步) // 支持 /training/ 和 /teacherTraining 两种路径 const isTrainingPage = location.pathname.includes('/training/') || location.pathname.includes('/teacherTraining'); if (isTrainingPage) { const domData = this._parsePeriodFromDOM(); if (domData) { this.state.earnedPeriod = domData.earned; this.state.certifiedPeriod = domData.certified; this.panel.updateEarned(this.state.earnedPeriod, CONFIG.TARGET_PERIOD); this.panel.updateCertified(this.state.certifiedPeriod, CONFIG.TARGET_PERIOD); this.panel.log(`📊 已学习: ${this.state.earnedPeriod.toFixed(2)} | 已认定: ${this.state.certifiedPeriod.toFixed(2)} / 目标: ${CONFIG.TARGET_PERIOD} (DOM同步)`); return; } } // 如果DOM抓取失败或不在研修页,回退到API const pd = await this.secondLearning.queryEarnedPeriod(this.state.userId, CONFIG.TRAIN_ID); this.state.earnedPeriod = pd._totalEarned || 0; this.state.certifiedPeriod = pd._certified || 0; this.panel.updateEarned(this.state.earnedPeriod, CONFIG.TARGET_PERIOD); this.panel.updateCertified(this.state.certifiedPeriod, CONFIG.TARGET_PERIOD); this.panel.log(`📊 已学习: ${this.state.earnedPeriod.toFixed(2)} | 已认定: ${this.state.certifiedPeriod.toFixed(2)} / 目标: ${CONFIG.TARGET_PERIOD} (API)`); } catch (err) { this.panel.log('⚠️ 学时刷新失败: ' + err.message); } } // 从页面DOM解析学时(首页适用) _parsePeriodFromDOM() { try { const text = document.body.innerText; // 匹配已学习学时:实际格式 "已学习\n17.27学时" const earnMatch = text.match(/已学习\s*([\d.]+)\s*学时/); // 匹配已认定学时:实际格式 "已认定 / 要求认定\n8.39 / 10学时" const certMatch = text.match(/已认定\s*\/\s*要求认定\s*([\d.]+)\s*\/\s*([\d.]+)\s*学时/); // 备选:其他可能的格式 const altCertMatch = text.match(/已认定\s*([\d.]+)\s*\/\s*([\d.]+)\s*学时/); if (earnMatch || certMatch || altCertMatch) { let certified = this.state.certifiedPeriod; let earned = this.state.earnedPeriod; if (earnMatch) { earned = parseFloat(earnMatch[1]); } if (certMatch) { certified = parseFloat(certMatch[1]); } else if (altCertMatch && altCertMatch[1] !== '要求认定') { // 确保不是匹配到 "已认定 / 要求认定" 这种 certified = parseFloat(altCertMatch[1]); } return { earned, certified }; } // 另一种备选:课程卡片中的汇总格式 const summaryMatch = text.match(/总进度[\s\S]*?已学习\s*([\d.]+)\s*学时[\s\S]*?已认定\s*\/\s*要求认定\s*([\d.]+)\s*\/\s*([\d.]+)/); if (summaryMatch) { return { earned: parseFloat(summaryMatch[1]), certified: parseFloat(summaryMatch[2]) }; } } catch (_) {} return null; } // 学时达标弹窗(以"已认定学时"达到10为准):继续学习 / 完成学习 → 返回 true=继续,false=完成 _maybeAskPeriod() { return new Promise((resolve) => { if (this.state.certifiedPeriod < CONFIG.TARGET_PERIOD) return resolve(true); if (GM_getValue(CONFIG.PERIOD_ASKED_KEY, '') === '1') return resolve(true); // 已选继续 // 弹窗询问 const overlay = document.createElement('div'); overlay.style.cssText = ` position:fixed;top:0;left:0;width:100%;height:100%; background:rgba(0,0,0,0.55);z-index:2147483647; display:flex;align-items:center;justify-content:center; font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; `; overlay.innerHTML = `
🎯

已完成规定认定学时

已认定 ${this.state.certifiedPeriod.toFixed(2)} / 目标 ${CONFIG.TARGET_PERIOD} 学时

是否继续学习剩余课程?

`; document.body.appendChild(overlay); overlay.querySelector('#sumuve-period-continue').addEventListener('click', () => { overlay.remove(); GM_setValue(CONFIG.PERIOD_ASKED_KEY, '1'); resolve(true); }); overlay.querySelector('#sumuve-period-finish').addEventListener('click', () => { overlay.remove(); this.stop(); this._showCompleteDialog(); resolve(false); }); }); } async _reportCourseProgress(course) { const progressData = { user_id: this.state.userId, resource_id: course.id, resource_name: course.name, resource_type: 't_course', progress: 100, status: 2, ext_info: JSON.stringify({ cv: 1, platform: 'web', tags: CONFIG.TAG, origin: CONFIG.TAG, additional_params: { library_id: CONFIG.LIBRARY_ID } }) }; try { await this.secondLearning.reportCourseProgress(CONFIG.TRAIN_ID, course.id, progressData); } catch (_) {} } async _checkComplete(forceCheck) { if (forceCheck) await this._refreshPeriod(); // 以"已认定学时"达到10为准 if (this.state.certifiedPeriod >= CONFIG.TARGET_PERIOD) { this._showCompleteDialog(); } else { this.panel.log(`⚠️ 认定学时未达标 (${this.state.certifiedPeriod.toFixed(2)}/${CONFIG.TARGET_PERIOD}),继续学习`); } } _showCompleteDialog() { this.state.running = false; const flow = this._loadFlowState(); flow.running = false; this._saveFlowState(flow); GM_setValue(CONFIG.AUTO_START_KEY, ''); this.panel.log('🎉 恭喜!认定学时已达标!'); this.autoLearning.stop(); // 隐藏进度面板(不显示小圆球,弹窗流程结束后自行处理) this.panel.hide(false); // 计算总用时 const totalSeconds = flow.startTime ? Math.floor((Date.now() - flow.startTime) / 1000) : 0; const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; const totalTimeStr = hours > 0 ? `${hours}时${minutes}分${seconds}秒` : `${minutes}分${seconds}秒`; const overlay = document.createElement('div'); overlay.style.cssText = ` position:fixed;top:0;left:0;width:100%;height:100%; background:rgba(0,0,0,0.5);z-index:2147483647; display:flex;align-items:center;justify-content:center; font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; `; overlay.innerHTML = `
🎉

恭喜!您已完成全部认定学时

已学习学时:${this.state.earnedPeriod.toFixed(2)}
已认定学时:${this.state.certifiedPeriod.toFixed(2)} / ${this.state.targetPeriod}
总用时:${totalTimeStr}

可关闭此页面,安心去做其他事情啦~

`; document.body.appendChild(overlay); overlay.querySelector('#sumuve-complete-ok').addEventListener('click', () => { overlay.remove(); this._showPromoDialog(); }); } // 完成后引流弹窗 _showPromoDialog() { const overlay = document.createElement('div'); overlay.style.cssText = ` position:fixed;top:0;left:0;width:100%;height:100%; background:rgba(0,0,0,0.5);z-index:2147483647; display:flex;align-items:center;justify-content:center; font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; `; overlay.innerHTML = `

学习已完成

脚本代开发请联系作者sumuve

`; document.body.appendChild(overlay); overlay.querySelector('#sumuve-promo-finish').addEventListener('click', () => { overlay.remove(); this._showFinishBall(); }); } // 完成后显示小圆球,点击重新显示欢迎弹窗 _showFinishBall() { const ball = document.createElement('div'); ball.id = 'sumuve-finish-ball'; const left = Math.max(10, window.innerWidth - 80); const top = Math.max(10, window.innerHeight / 2 - 30); ball.style.cssText = ` position:fixed;left:${left}px;top:${top}px; width:60px;height:60px;border-radius:50%; background:linear-gradient(135deg,#22c55e,#16a34a); box-shadow:0 6px 20px rgba(34,197,94,0.4); display:flex;align-items:center;justify-content:center; z-index:2147483646;cursor:pointer; user-select:none;transition:transform 0.15s ease,box-shadow 0.15s ease; animation:sumuveBallPop 0.3s ease; `; ball.innerHTML = ``; ball.title = '点击重新开始学习'; document.body.appendChild(ball); // 拖拽和点击逻辑 let isDragging = false, moved = false, startX = 0, startY = 0, initLeft = 0, initTop = 0; ball.addEventListener('click', (e) => { if (moved) { moved = false; return; } // 点击:显示欢迎弹窗 ball.remove(); const welcome = new WelcomeDialog(); welcome.show(); }); ball.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); const rect = ball.getBoundingClientRect(); isDragging = true; moved = false; startX = e.clientX; startY = e.clientY; initLeft = rect.left; initTop = rect.top; ball.style.transition = 'none'; ball.style.transform = 'scale(0.95)'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; let nx = initLeft + dx; let ny = initTop + dy; nx = Math.max(0, Math.min(window.innerWidth - 60, nx)); ny = Math.max(0, Math.min(window.innerHeight - 60, ny)); ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; ball.style.transform = ''; ball.style.transition = 'transform 0.15s ease,box-shadow 0.15s ease'; const rect = ball.getBoundingClientRect(); const cx = rect.left + rect.width / 2; let targetLeft = rect.left; if (cx < window.innerWidth / 2) targetLeft = 10; else targetLeft = window.innerWidth - 70; ball.style.transition = 'left 0.25s ease,transform 0.15s ease,box-shadow 0.15s ease'; ball.style.left = targetLeft + 'px'; ball.style.right = 'auto'; }); // 触屏支持 ball.addEventListener('touchstart', (e) => { if (e.touches.length !== 1) return; const t = e.touches[0]; const rect = ball.getBoundingClientRect(); isDragging = true; moved = false; startX = t.clientX; startY = t.clientY; initLeft = rect.left; initTop = rect.top; ball.style.transition = 'none'; }, { passive: true }); document.addEventListener('touchmove', (e) => { if (!isDragging || e.touches.length !== 1) return; const t = e.touches[0]; const dx = t.clientX - startX; const dy = t.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; let nx = initLeft + dx; let ny = initTop + dy; nx = Math.max(0, Math.min(window.innerWidth - 60, nx)); ny = Math.max(0, Math.min(window.innerHeight - 60, ny)); ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }, { passive: true }); document.addEventListener('touchend', () => { if (!isDragging) return; isDragging = false; const rect = ball.getBoundingClientRect(); const cx = rect.left + rect.width / 2; let targetLeft = rect.left; if (cx < window.innerWidth / 2) targetLeft = 10; else targetLeft = window.innerWidth - 70; ball.style.transition = 'left 0.25s ease,transform 0.15s ease'; ball.style.left = targetLeft + 'px'; }); } async _waitIfPaused() { // 持久化暂停:从GM存储读取暂停状态 // 遇到停止标记立即返回 false 中断调用者 while (true) { if (GM_getValue(CONFIG.STOPPED_KEY, '') === '1') return false; if (GM_getValue(CONFIG.PAUSED_KEY, '') !== '1') break; this.state.paused = true; await Utils.wait(500); } this.state.paused = false; return this._checkContinue(); } // 快速检查是否可继续(state.running && 未点停止) _checkContinue() { if (GM_getValue(CONFIG.STOPPED_KEY, '') === '1') return false; return !!this.state.running; } togglePause() { this.state.paused = !this.state.paused; GM_setValue(CONFIG.PAUSED_KEY, this.state.paused ? '1' : ''); this.panel.updatePauseButton(this.state.paused ? 'paused' : 'running'); this.panel.log(this.state.paused ? '⏸️ 已暂停' : '▶️ 已继续'); // 暂停时立即停止当前视频播放 if (this.state.paused) this.autoLearning.stop(); } stop() { // 先写停止标记,所有异步循环都会立即中断 GM_setValue(CONFIG.STOPPED_KEY, '1'); this.state.running = false; this.state.paused = false; GM_setValue(CONFIG.PAUSED_KEY, ''); // 立即停止 watchdog + 暂停页面上所有 video this.autoLearning.stop(); // 清除自动播放相关定时器 this._clearAutoPlayTimers(); // 清除自动启动标记(停止后不自动恢复) GM_setValue(CONFIG.AUTO_START_KEY, ''); // 重置 flow.running,确保刷新页面后不会自动恢复 // 但保留其他断点数据(courseIdx, autoLearnIdx等),以便下次手动开始时继续 try { const flow = this._loadFlowState(); flow.running = false; this._saveFlowState(flow); } catch (_) {} this.panel.updatePauseButton('idle'); this.panel.log('🛑 已停止,点击"开始"继续学习'); } } // ==================== 初始化入口 ==================== // 立即显示面板并查询学时(带重试) async function showPanelAndQueryPeriod() { const panel = ProgressPanel.getInstance(); panel.show(); const isCourseDetailPage = location.pathname.includes('/teacherTraining/courseDetail'); // 优先从DOM抓取(最准确,与首页显示同步) // 注意:课程详情页不抓取DOM,避免抓到单个课程的学时而非总学时 const tryDom = () => { try { const text = document.body.innerText; const earnMatch = text.match(/已学习\s*([\d.]+)\s*学时/); const certMatch = text.match(/已认定\s*\/\s*要求认定\s*([\d.]+)\s*\/\s*([\d.]+)\s*学时/); if (earnMatch || certMatch) { const earned = earnMatch ? parseFloat(earnMatch[1]) : null; const certified = certMatch ? parseFloat(certMatch[1]) : null; return { earned, certified }; } } catch (_) {} return null; }; // 非课程详情页:立即尝试一次DOM抓取 let domData = null; if (!isCourseDetailPage) { domData = tryDom(); if (domData && domData.earned !== null) { panel.updateEarned(domData.earned, CONFIG.TARGET_PERIOD); } if (domData && domData.certified !== null) { panel.updateCertified(domData.certified, CONFIG.TARGET_PERIOD); } if (domData && (domData.earned !== null || domData.certified !== null)) { const e = domData.earned !== null ? domData.earned.toFixed(2) : '--'; const c = domData.certified !== null ? domData.certified.toFixed(2) : '--'; panel.log(`📊 已学习 ${e} 学时 | 已认定 ${c}/${CONFIG.TARGET_PERIOD} 学时 (DOM)`); } } // 查询API(带重试,确保token准备好) // 课程详情页直接回退到API;首页如果DOM抓取失败也回退到API for (let retry = 0; retry < 5; retry++) { try { const tokenInfo = Utils.getDynamicToken(); const userId = Utils.getUserId(); if (tokenInfo && userId) { const sl = new SecondLearning(); sl.tokenInfo = tokenInfo; const periodData = await sl.queryEarnedPeriod(userId, CONFIG.TRAIN_ID); // 非课程详情页:优先使用DOM数据(API可能滞后),DOM无数据则用API let earned, cert; if (!isCourseDetailPage) { const freshDom = tryDom(); earned = (freshDom && freshDom.earned !== null) ? freshDom.earned : (periodData._totalEarned || 0); cert = (freshDom && freshDom.certified !== null) ? freshDom.certified : (periodData._certified || 0); } else { // 课程详情页:直接使用API数据 earned = periodData._totalEarned || 0; cert = periodData._certified || 0; } panel.updateEarned(earned, CONFIG.TARGET_PERIOD); panel.updateCertified(cert, CONFIG.TARGET_PERIOD); panel.log(`📊 已学习 ${earned.toFixed(2)} 学时 | 已认定 ${cert.toFixed(2)}/${CONFIG.TARGET_PERIOD} 学时 (API)`); return; } } catch (_) {} await Utils.wait(1000); } } function init() { const autoStart = GM_getValue(CONFIG.AUTO_START_KEY, ''); const path = location.pathname; const isTrainingPage = path.includes('/training') || path.includes('/teacherTraining') || path.includes('/x-course'); // 检查是否有真正的进行中流程(AUTO_START + flow.running 双条件) let flowRunning = false; if (autoStart === '1') { try { const raw = GM_getValue(CONFIG.FLOW_STATE_KEY, ''); if (raw) { const f = JSON.parse(raw); flowRunning = !!f.running; } } catch (_) {} if (!flowRunning) { // AUTO_START 残留但流程已结束 → 清除残留 GM_setValue(CONFIG.AUTO_START_KEY, ''); } } // 自动恢复仅限课程详情页(跨页面跳转用),首页/入口页永远手动启动 const isCourseDetailPage = path.includes('/teacherTraining/courseDetail'); if (autoStart === '1' && flowRunning && isCourseDetailPage) { // 自动恢复模式:在课程详情页有进行中的流程,恢复执行 const run = () => { showPanelAndQueryPeriod(); // 同时启动学时同步监控(即使在运行模式下也需要检测达标) startPeriodSyncMonitor(); setTimeout(() => { MainController.getInstance().start(); }, 2000); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { run(); } return; } // 非自动恢复场景:如果AUTO_START残留但不在详情页,清除它 if (autoStart === '1' && !isCourseDetailPage) { GM_setValue(CONFIG.AUTO_START_KEY, ''); } // 首页:显示欢迎弹窗,隐藏进度面板 const welcome = new WelcomeDialog(); if (welcome.shouldShow()) { // 确保进度面板在首页隐藏(不显示小圆球) const panel = ProgressPanel.getInstance(); panel.hide(false); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => welcome.show()); } else { welcome.show(); } } else if (isTrainingPage && !isCourseDetailPage) { // 研修相关页面(非首页、非详情页):常驻显示进度面板并立即查询学时 const run = () => { const panel = ProgressPanel.getInstance(); panel.show(); panel.updatePauseButton('idle'); panel.log('💡 进度面板已就绪,点击"秒学"按钮开始学习'); showPanelAndQueryPeriod(); // 启动持续同步监控:每3秒检查一次DOM并自动更新面板 startPeriodSyncMonitor(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { run(); } } else if (isCourseDetailPage) { // 课程详情页:也显示进度面板并启动学时同步 const run = () => { const panel = ProgressPanel.getInstance(); panel.show(); panel.updatePauseButton('idle'); panel.log('💡 课程详情页 - 进度面板已就绪'); showPanelAndQueryPeriod(); // 启动持续同步监控(课程详情页使用API回退) startPeriodSyncMonitor(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { run(); } } } // 持续同步学时监控(确保页面数据变更时自动同步到面板) let _periodSyncTimer = null; let _lastSynced = { earned: -1, certified: -1 }; let _hasShownCompleteDialog = false; // 防重复弹窗标记 function startPeriodSyncMonitor() { if (_periodSyncTimer) clearInterval(_periodSyncTimer); const syncOnce = async () => { const panel = ProgressPanel.getInstance(); if (!panel || !panel.panel) return; try { const isCourseDetailPage = location.pathname.includes('/teacherTraining/courseDetail'); let earned = null, certified = null; let source = 'API'; if (!isCourseDetailPage) { // 非课程详情页(首页/研修专题页/视频播放页):优先从DOM抓取总学时 const text = document.body.innerText; // 严格匹配总进度格式:"已学习 X.XX 学时" 和 "已认定 / 要求认定 X.XX / X.XX 学时" const earnMatch = text.match(/已学习\s*([\d.]+)\s*学时/); const certMatch = text.match(/已认定\s*\/\s*要求认定\s*([\d.]+)\s*\/\s*([\d.]+)\s*学时/); if (earnMatch) earned = parseFloat(earnMatch[1]); if (certMatch) certified = parseFloat(certMatch[1]); source = 'DOM'; } // 课程详情页 或 DOM抓取失败时:回退到API if (earned === null || certified === null) { try { const tokenInfo = Utils.getDynamicToken(); const userId = Utils.getUserId(); if (tokenInfo && userId) { const sl = new SecondLearning(); sl.tokenInfo = tokenInfo; const periodData = await sl.queryEarnedPeriod(userId, CONFIG.TRAIN_ID); if (earned === null) earned = periodData._totalEarned || 0; if (certified === null) certified = periodData._certified || 0; } } catch (_) {} } // 仅在数值变化时更新(避免无谓刷新) if (earned !== null && earned !== _lastSynced.earned) { panel.updateEarned(earned, CONFIG.TARGET_PERIOD); _lastSynced.earned = earned; } if (certified !== null && certified !== _lastSynced.certified) { panel.updateCertified(certified, CONFIG.TARGET_PERIOD); _lastSynced.certified = certified; // 检测已认定学时是否达到10:无论是否在运行学习任务,只要达标就弹窗 if (certified >= CONFIG.TARGET_PERIOD && !_hasShownCompleteDialog) { _hasShownCompleteDialog = true; // 标记已弹窗,避免重复 const mc = MainController.getInstance(); if (mc.state.running) { // 如果正在运行,先停止再弹窗 panel.log(`🎉 已认定学时达到 ${certified.toFixed(2)},停止学习`); mc.stop(); mc._showCompleteDialog(); } else { // 未运行学习任务时,独立弹出完成提示 panel.log(`🎉 已认定学时达到 ${certified.toFixed(2)}/${CONFIG.TARGET_PERIOD},认定学时已完成`); showPeriodCompleteDialog(earned || 0, certified); } // 隐藏进度面板(不显示小圆球,弹窗流程结束后自行处理) panel.hide(false); } } if (earned !== null || certified !== null) { const e = earned !== null ? earned.toFixed(2) : '--'; const c = certified !== null ? certified.toFixed(2) : '--'; panel.log(`🔄 同步: 已学习 ${e} 学时 | 已认定 ${c}/${CONFIG.TARGET_PERIOD} 学时 (${source})`); } } catch (_) {} }; // 立即执行一次 syncOnce(); // 每3秒检查一次 _periodSyncTimer = setInterval(syncOnce, 3000); } // 独立的完成认定学时弹窗(非运行学习任务时调用) function showPeriodCompleteDialog(earnedPeriod, certifiedPeriod) { // 读取流程状态中的开始时间计算总用时 let totalTimeStr = '0分0秒'; try { const raw = GM_getValue(CONFIG.FLOW_STATE_KEY, ''); if (raw) { const flow = JSON.parse(raw); if (flow.startTime) { const totalSeconds = Math.floor((Date.now() - flow.startTime) / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; totalTimeStr = hours > 0 ? `${hours}时${minutes}分${seconds}秒` : `${minutes}分${seconds}秒`; } } } catch (_) {} const overlay = document.createElement('div'); overlay.style.cssText = ` position:fixed;top:0;left:0;width:100%;height:100%; background:rgba(0,0,0,0.5);z-index:2147483647; display:flex;align-items:center;justify-content:center; font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; `; overlay.innerHTML = `
🎊

您已完成规定认定学时

已学习学时:${earnedPeriod.toFixed(2)}
已认定学时:${certifiedPeriod.toFixed(2)} / ${CONFIG.TARGET_PERIOD}
总用时:${totalTimeStr}

恭喜!已达到规定的10个认定学时要求

`; document.body.appendChild(overlay); overlay.querySelector('#sumuve-period-complete-ok').addEventListener('click', () => { overlay.remove(); // 弹窗关闭后显示引流弹窗(与主流程保持一致) showPromoDialogStandalone(); }); } // 独立的引流弹窗(非运行学习任务时调用) function showPromoDialogStandalone() { const overlay = document.createElement('div'); overlay.style.cssText = ` position:fixed;top:0;left:0;width:100%;height:100%; background:rgba(0,0,0,0.5);z-index:2147483647; display:flex;align-items:center;justify-content:center; font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; `; overlay.innerHTML = `

学习已完成

脚本代开发请联系作者sumuve

`; document.body.appendChild(overlay); overlay.querySelector('#sumuve-promo-finish-standalone').addEventListener('click', () => { overlay.remove(); // 点击结束后显示小圆球 showFinishBallAfterComplete(); }); } // 完成后显示小圆球(独立函数,供非运行状态时调用) function showFinishBallAfterComplete() { const ball = document.createElement('div'); ball.id = 'sumuve-finish-ball'; const left = Math.max(10, window.innerWidth - 80); const top = Math.max(10, window.innerHeight / 2 - 30); ball.style.cssText = ` position:fixed;left:${left}px;top:${top}px; width:60px;height:60px;border-radius:50%; background:linear-gradient(135deg,#22c55e,#16a34a); box-shadow:0 6px 20px rgba(34,197,94,0.4); display:flex;align-items:center;justify-content:center; z-index:2147483646;cursor:pointer; user-select:none;transition:transform 0.15s ease,box-shadow 0.15s ease; animation:sumuveBallPop 0.3s ease; `; ball.innerHTML = ``; ball.title = '点击重新开始学习'; document.body.appendChild(ball); // 拖拽和点击逻辑 let isDragging = false, moved = false, startX = 0, startY = 0, initLeft = 0, initTop = 0; ball.addEventListener('click', (e) => { if (moved) { moved = false; return; } ball.remove(); const welcome = new WelcomeDialog(); welcome.fromComplete = true; // 标记来自完成学时流程 welcome.show(); }); ball.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); const rect = ball.getBoundingClientRect(); isDragging = true; moved = false; startX = e.clientX; startY = e.clientY; initLeft = rect.left; initTop = rect.top; ball.style.transition = 'none'; ball.style.transform = 'scale(0.95)'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; let nx = initLeft + dx; let ny = initTop + dy; nx = Math.max(0, Math.min(window.innerWidth - 60, nx)); ny = Math.max(0, Math.min(window.innerHeight - 60, ny)); ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; ball.style.transform = ''; ball.style.transition = 'transform 0.15s ease,box-shadow 0.15s ease'; const rect = ball.getBoundingClientRect(); const cx = rect.left + rect.width / 2; let targetLeft = rect.left; if (cx < window.innerWidth / 2) targetLeft = 10; else targetLeft = window.innerWidth - 70; ball.style.transition = 'left 0.25s ease,transform 0.15s ease,box-shadow 0.15s ease'; ball.style.left = targetLeft + 'px'; ball.style.right = 'auto'; }); // 触屏支持 ball.addEventListener('touchstart', (e) => { if (e.touches.length !== 1) return; const t = e.touches[0]; const rect = ball.getBoundingClientRect(); isDragging = true; moved = false; startX = t.clientX; startY = t.clientY; initLeft = rect.left; initTop = rect.top; ball.style.transition = 'none'; }, { passive: true }); document.addEventListener('touchmove', (e) => { if (!isDragging || e.touches.length !== 1) return; const t = e.touches[0]; const dx = t.clientX - startX; const dy = t.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; let nx = initLeft + dx; let ny = initTop + dy; nx = Math.max(0, Math.min(window.innerWidth - 60, nx)); ny = Math.max(0, Math.min(window.innerHeight - 60, ny)); ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; ball.style.right = 'auto'; ball.style.bottom = 'auto'; }, { passive: true }); document.addEventListener('touchend', () => { if (!isDragging) return; isDragging = false; const rect = ball.getBoundingClientRect(); const cx = rect.left + rect.width / 2; let targetLeft = rect.left; if (cx < window.innerWidth / 2) targetLeft = 10; else targetLeft = window.innerWidth - 70; ball.style.transition = 'left 0.25s ease,transform 0.15s ease'; ball.style.left = targetLeft + 'px'; }); } // 启动 init(); })();