// ==UserScript== // @name 知学云自动化学习辅助工具-V1.0.3稳定版 // @namespace http://tampermonkey.net/ // @version 1.0.3 // @description 适配 Vue+AntDesign 架构。 // @author Advanced_JS_Bot // @match *://kc.zhixueyun.com/* // @grant unsafeWindow // @run-at document-end // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/557317/%E7%9F%A5%E5%AD%A6%E4%BA%91%E8%87%AA%E5%8A%A8%E5%8C%96%E5%AD%A6%E4%B9%A0%E8%BE%85%E5%8A%A9%E5%B7%A5%E5%85%B7-V103%E7%A8%B3%E5%AE%9A%E7%89%88.user.js // @updateURL https://update.greasyfork.icu/scripts/557317/%E7%9F%A5%E5%AD%A6%E4%BA%91%E8%87%AA%E5%8A%A8%E5%8C%96%E5%AD%A6%E4%B9%A0%E8%BE%85%E5%8A%A9%E5%B7%A5%E5%85%B7-V103%E7%A8%B3%E5%AE%9A%E7%89%88.meta.js // ==/UserScript== (function() { 'use strict'; if (window !== window.top) return; // === 配置参数 === const CONFIG = { heartbeat: 3000, maxBtnTextLength: 20, keywords: { start: ['开始学习', '继续学习'], more: ['查看更多', '加载更多', 'More'], popup: ['知道', '关闭', '继续', '下一节'] }, excludeWords: ['未完成', '已完成', '学时', '状态', '时间', '必修', '选修'], selectors: { iframe: '#paasIframe', clickable: '.ant-btn, button, .btn, a, div[role="button"], span, div' } }; const STATE = { isClicking: false, learningStarted: false, // 核心标志:是否正在学习 statusText: '初始化...', docCount: 0, blockedCourses: {} }; // === 课程锁定机制 (辅助防止短期重复) === const blockCourse = (text) => { STATE.blockedCourses[text] = Date.now() + 60000; const stored = JSON.parse(sessionStorage.getItem('zxy_blocked_list') || '{}'); stored[text] = Date.now() + 60000; sessionStorage.setItem('zxy_blocked_list', JSON.stringify(stored)); }; const isBlocked = (text) => { const now = Date.now(); if (STATE.blockedCourses[text] && STATE.blockedCourses[text] > now) return true; const stored = JSON.parse(sessionStorage.getItem('zxy_blocked_list') || '{}'); if (stored[text] && stored[text] > now) return true; return false; }; const getBlockedCount = () => { const now = Date.now(); const stored = JSON.parse(sessionStorage.getItem('zxy_blocked_list') || '{}'); let count = 0; for (let k in stored) { if (stored[k] > now) count++; } return count; }; // === 状态面板 (UI) === const createStatusPanel = () => { let panel = document.getElementById('zxy_status_panel'); if (!panel) { panel = document.createElement('div'); panel.id = 'zxy_status_panel'; Object.assign(panel.style, { position: 'fixed', bottom: '10px', right: '10px', zIndex: '999999', backgroundColor: 'rgba(0, 0, 0, 0.85)', color: '#fff', padding: '12px', borderRadius: '8px', fontSize: '12px', pointerEvents: 'none', userSelect: 'none', lineHeight: '1.6', boxShadow: '0 0 10px rgba(0,0,0,0.5)', fontFamily: 'monospace' }); document.body.appendChild(panel); } return panel; }; const updateStatus = (text, subText = '') => { const panel = createStatusPanel(); STATE.statusText = text; const blockedCount = getBlockedCount(); panel.innerHTML = `
知学云自动化学习辅助工具-V1.0.3
状态: ${text}
${subText || `文档: ${STATE.docCount} | 锁定: ${blockedCount}`}
`; }; const log = (msg, type = 'info') => { const colors = { success: '#32CD32', warn: '#FFA500', error: '#FF0000', info: '#00BFFF' }; console.log(`%c[全自动刷课] ${msg}`, `color: ${colors[type] || colors.info}; font-weight: bold;`); }; const checkCoolDown = () => { const lastClick = sessionStorage.getItem('zxy_last_click_time'); if (lastClick && Date.now() - parseInt(lastClick) < 3000) return true; return false; }; const getAllDocuments = () => { const docs = [document]; const paasIframe = document.querySelector(CONFIG.selectors.iframe) || document.querySelector('iframe[src*="paas"]'); if (paasIframe) { try { const iDoc = paasIframe.contentDocument || paasIframe.contentWindow.document; if (iDoc) docs.push(iDoc); } catch (e) {} } document.querySelectorAll('iframe').forEach(ifr => { if (ifr !== paasIframe) { try { const iDoc = ifr.contentDocument || ifr.contentWindow.document; if (iDoc && !docs.includes(iDoc)) docs.push(iDoc); } catch (e) {} } }); STATE.docCount = docs.length; return docs; }; const findInteractiveParent = (element) => { const target = element.closest('button, a, .ant-btn, input[type="button"]'); if (target && !target.disabled) return target; let current = element; try { const win = element.ownerDocument.defaultView || window; for (let i = 0; i < 3; i++) { if (!current || current === element.ownerDocument.body) break; const style = win.getComputedStyle(current); if (style.cursor === 'pointer' || current.getAttribute('role') === 'button') { return current; } current = current.parentElement; } } catch (e) {} return element; }; const simulateClick = (element) => { if (!element) return; log(`点击 -> [${element.innerText.replace(/\s+/g, '')}]`, 'info'); updateStatus(`正在打开: ${element.innerText.substring(0, 8)}...`); try { if (['BUTTON', 'A', 'INPUT'].includes(element.tagName)) { element.click(); } else { element.click(); element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, buttons: 1 })); } } catch(e) {} }; const matchesKeyword = (text, keywords) => { if (!text) return false; text = text.trim(); if (CONFIG.excludeWords.some(bad => text.includes(bad))) return false; return text.length > 1 && text.length <= CONFIG.maxBtnTextLength && keywords.some(kw => text.includes(kw)); }; // === 核心:列表页处理逻辑 === const handleListPage = (docs) => { // 1. 如果已经开始学习,强制停止扫描 if (STATE.learningStarted) { updateStatus('正在学习中...', '等待视频关闭...'); return; // 终止后续逻辑 } if (checkCoolDown()) { updateStatus('冷却中...', '防止连点'); return; } let potentialTargets = []; for (const doc of docs) { const elements = Array.from(doc.querySelectorAll(CONFIG.selectors.clickable)); elements.forEach(el => { if (el.offsetParent === null) return; if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(el.tagName)) return; const text = el.innerText || el.textContent || ""; if (matchesKeyword(text, CONFIG.keywords.start)) { if (CONFIG.excludeWords.some(bad => text.includes(bad))) return; if (isBlocked(text.trim())) return; // 检查锁定 const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { potentialTargets.push({ element: el, top: rect.top, text: text.trim(), tagName: el.tagName }); } } }); } updateStatus(potentialTargets.length > 0 ? '发现未学课程' : '扫描中...', potentialTargets.length); if (potentialTargets.length > 0) { potentialTargets.sort((a, b) => { const tagScore = { 'BUTTON': 0, 'A': 1, 'SPAN': 2, 'DIV': 3 }; const scoreA = tagScore[a.tagName] ?? 4; const scoreB = tagScore[b.tagName] ?? 4; if (scoreA !== scoreB) return scoreA - scoreB; const diffTop = a.top - b.top; if (diffTop < -10) return -1; if (diffTop > 10) return 1; return a.text.length - b.text.length; }); const bestCandidate = potentialTargets[0].element; const bestText = potentialTargets[0].text; const realBtn = findInteractiveParent(bestCandidate); if (realBtn) { log(`锁定并点击: "${bestText}"`, 'success'); blockCourse(bestText); // === 关键操作:标记开始学习 === STATE.isClicking = true; STATE.learningStarted = true; sessionStorage.setItem('zxy_last_click_time', Date.now()); // 重置心跳,防止误判之前的残留心跳 localStorage.setItem('zxy_player_heartbeat', '0'); simulateClick(realBtn); } return; } // 加载更多 for (const doc of docs) { const elements = Array.from(doc.querySelectorAll(CONFIG.selectors.clickable)); const moreBtn = elements.find(el => { if (el.offsetParent === null) return false; const text = el.innerText || ""; return !CONFIG.excludeWords.some(bad => text.includes(bad)) && matchesKeyword(text.replace(/\s/g, ''), CONFIG.keywords.more); }); if (moreBtn) { const realBtn = findInteractiveParent(moreBtn); updateStatus('点击加载更多'); STATE.isClicking = true; simulateClick(realBtn); setTimeout(() => { STATE.isClicking = false; }, 3000); return; } } }; // === 播放页处理逻辑 === const handlePlayerPage = (docs) => { updateStatus('视频播放中...'); // === 发送心跳信号 (告诉列表页我还在) === localStorage.setItem('zxy_player_heartbeat', Date.now()); let videoFound = false; for (const doc of docs) { const video = doc.querySelector('video'); if (video) { videoFound = true; if (video.paused) { video.muted = true; video.play().catch(()=>{}); } } } docs.forEach(doc => { const allTexts = Array.from(doc.querySelectorAll('span, div, button')); const popupBtn = allTexts.find(el => { const t = el.innerText; return t && t.length < 10 && matchesKeyword(t, CONFIG.keywords.popup) && el.offsetParent !== null; }); if (popupBtn) { updateStatus('关闭弹窗'); simulateClick(findInteractiveParent(popupBtn)); } const textContent = doc.body.innerText || ""; if (textContent.includes('本节播放结束') || textContent.includes('学完当前课程')) { log('本节完成,即将关闭窗口...', 'success'); updateStatus('完成,准备刷新列表'); // 1. 尝试刷新父页面 (列表页) try { if(window.opener) window.opener.location.reload(); } catch(e) {} // 2. 关闭自己 setTimeout(() => window.close(), 500); } }); }; // === 监听页面可见性 (列表页刷新逻辑 - 心跳增强版) === document.addEventListener('visibilitychange', () => { // 只有当页面重新可见,且处于学习状态时才检查 if (!document.hidden && STATE.learningStarted) { // 检查心跳 const lastBeat = parseInt(localStorage.getItem('zxy_player_heartbeat') || '0'); const timeDiff = Date.now() - lastBeat; // 如果心跳在 6秒内,说明视频页还在运行,此时不刷新! if (timeDiff < 6000) { log('检测到视频页仍在前台运行,暂不刷新列表', 'info'); updateStatus('视频播放中,暂不刷新'); return; } log('检测到视频页已关闭 (心跳中断),执行刷新...', 'warn'); updateStatus('学习结束,刷新列表...'); setTimeout(() => window.location.reload(), 500); } }); const mainTick = () => { const docs = getAllDocuments(); if (window.location.href.includes('course/detail') || window.location.href.includes('play')) { handlePlayerPage(docs); } else { handleListPage(docs); } }; log('全自动刷课脚本 v1.0.3 已启动 (Heartbeat Mode)', 'success'); setInterval(mainTick, CONFIG.heartbeat); })();