// ==UserScript== // @name 厦门理工高校邦刷课脚本(视频+页面+讨论) // @namespace https://github.com/Wu557666/gaoxiaobang // @version 2.3.2 // @description 针对厦门理工高校邦暴力极速刷课 // @author wu某人 // @icon https://favicon.im/xmut.gaoxiaobang.com?size=128 // @match https://xmut.class.gaoxiaobang.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant unsafeWindow // @run-at document-idle // ==/UserScript== (function() { 'use strict'; const $ = unsafeWindow.$ || window.$; const wait = ms => new Promise(r => setTimeout(r, ms)); const STATE_KEY = 'gb_auto_step'; const isChapterPage = !!(unsafeWindow.classinfo?.classId && (unsafeWindow.unitList || unsafeWindow.chapterList)); const isDiscussPage = () => !!document.querySelector('a[content_type="Topic"]'); // 全局标记,防止重复执行讨论 let discussRunning = false; // ========== 第一部分:刷进度 ========== async function runProgress() { console.log('%c📚 第一步:开始刷视频/页面进度(极速并发)', 'color:#1fb6ff;font-size:16px'); const urlPrefix = location.protocol + '//' + location.host; const classId = unsafeWindow.classinfo.classId; function extractAllChapters(source) { let result = []; if (!source) return result; if (Array.isArray(source)) { source.forEach(item => { if (item.contentType) result.push(item); if (item.itemList) result = result.concat(extractAllChapters(item.itemList)); if (item.chapterList) result = result.concat(extractAllChapters(item.chapterList)); }); } else if (typeof source === 'object') { if (source.contentType) result.push(source); if (source.itemList) result = result.concat(extractAllChapters(source.itemList)); if (source.chapterList) result = result.concat(extractAllChapters(source.chapterList)); } return result; } const dataSource = unsafeWindow.unitList || unsafeWindow.chapterList || unsafeWindow.courseData || unsafeWindow.chapters || unsafeWindow.data; if (!dataSource) { console.error('❌ 未找到章节数据源,请手动展开目录后刷新页面'); return; } const allChapters = extractAllChapters(dataSource); const videoChapters = allChapters.filter(c => c.contentType === 'Video'); const pageChapters = allChapters.filter(c => ['Page', 'UEditor', 'Html'].includes(c.contentType)); console.log(`📹 视频章节: ${videoChapters.length} 个`); console.log(`📄 页面章节: ${pageChapters.length} 个`); // 视频并发 const videoPromises = videoChapters.map(chapter => new Promise(resolve => { $.ajax({ url: `${urlPrefix}/class/${classId}/chapter/${chapter.chapterId}/api`, type: 'GET', success: result => { const seconds = (result.chapter?.video?.seconds) || 0; $.ajax({ url: `${urlPrefix}/log/video/${chapter.chapterId}/${classId}/api`, type: 'POST', data: { data: JSON.stringify([{ state: "listening", level: 2, ch: seconds, mh: 0 }]) }, success: () => { console.log(`✅ 视频 ${chapter.chapterId} 上报成功`); resolve(); }, error: xhr => { console.error(`❌ 视频 ${chapter.chapterId} 上报失败 (${xhr.status})`); resolve(); } }); }, error: xhr => { console.error(`❌ 视频 ${chapter.chapterId} 内容获取失败 (${xhr.status})`); resolve(); } }); })); // 页面并发 pageChapters.forEach((chapter, index) => { $.ajax({ url: `${urlPrefix}/class/${classId}/chapter/${chapter.chapterId}/api?${Date.now()}`, type: 'GET', success: () => console.log(`🟢 页面 ${chapter.chapterId} GET成功 (${index+1}/${pageChapters.length})`), error: xhr => console.warn(`🔴 页面 ${chapter.chapterId} GET失败 (${xhr.status}),但可能仍有效`) }); }); await Promise.all(videoPromises); console.log(`📄 页面章节 ${pageChapters.length} 个GET请求已全部发出(并发)`); console.log('%c✅ 进度刷取完成,立即跳转讨论区', 'color:green;font-size:14px'); GM_setValue(STATE_KEY, 'discuss'); // 开始主动轮询检测讨论区 startPollingForDiscuss(); // 跳转 const firstTopic = document.querySelector('a[content_type="Topic"]'); if (firstTopic) { const chapterId = firstTopic.getAttribute('chapter_id'); if (chapterId) { location.hash = location.hash.replace(/chapterId=\d+/, `chapterId=${chapterId}`); console.log(`🚀 已通过 hash 跳转到专题讨论 (chapterId=${chapterId})`); } else { firstTopic.click(); console.log('🚀 已通过点击跳转到专题讨论'); } } else { console.warn('未找到专题讨论入口,请手动点击进入'); } } // ========== 主动轮询检测讨论区 ========== let pollingTimer = null; let pollStartTime = null; const POLL_TIMEOUT = 15000; // 15秒超时 function startPollingForDiscuss() { if (pollingTimer) clearInterval(pollingTimer); pollStartTime = Date.now(); pollingTimer = setInterval(async () => { const step = GM_getValue(STATE_KEY, 'progress'); if (step !== 'discuss') { clearInterval(pollingTimer); return; } // 检测到讨论区元素且未在执行 if (isDiscussPage() && !discussRunning) { clearInterval(pollingTimer); console.log('%c🔔 检测到讨论区已加载,继续执行第二步...', 'color:#1fb6ff;font-size:14px'); await runDiscuss(); return; } // 超时提示 if (Date.now() - pollStartTime > POLL_TIMEOUT) { clearInterval(pollingTimer); console.warn('%c⚠️ 等待讨论区加载超时!请手动在控制台输入 autoContinue() 继续,或刷新页面。', 'color:orange;font-size:14px'); window.autoContinue = async () => { if (isDiscussPage() && !discussRunning) { await runDiscuss(); } else { console.error('当前页面不是讨论区或已在执行中'); } }; console.log('%c👉 手动继续命令已挂载:autoContinue()', 'color:yellow'); } }, 800); } // ========== 第二部分:讨论区批量评论(修复可见性检测) ========== async function runDiscuss() { if (discussRunning) return; discussRunning = true; console.log('%c💬 第二步:开始批量评论', 'color:#1fb6ff;font-size:16px'); const Editor = { // 获取 UEditor 实例(增强兼容性) getUEditor() { // 方式1:全局 UE.instants const UE = unsafeWindow.UE || window.UE; if (UE?.instants) { const inst = Object.values(UE.instants).find(i => i && i.setContent); if (inst) return inst; } // 方式2:通过 UE.getEditor if (UE && typeof UE.getEditor === 'function') { const inst = UE.getEditor('ueditor'); if (inst && inst.setContent) return inst; } // 方式3:遍历所有 iframe 对应的实例 if (UE && UE.instants) { for (let k in UE.instants) { if (UE.instants[k] && UE.instants[k].setContent) return UE.instants[k]; } } return null; }, setContent(content) { const ue = this.getUEditor(); if (ue) { ue.setContent(content); try { ue.fireEvent('contentChange'); } catch(e) {} console.log(' ✅ 通过 UEditor API 填入'); return true; } // 降级:直接操作 contenteditable body const iframe = document.querySelector('iframe[id^="ueditor_"]'); if (iframe) { try { const doc = iframe.contentDocument || iframe.contentWindow.document; const body = doc.querySelector('body[contenteditable="true"]'); if (body) { body.innerHTML = `

${content.replace(/\n/g, '

')}

`; body.dispatchEvent(new Event('input', { bubbles: true })); console.log(' ✅ 通过 iframe body 填入'); return true; } } catch(e) {} } return false; }, getLongestComment() { const texts = []; // 精准定位:只在 .reply-content 容器内查找直接子元素或内部的 p document.querySelectorAll('.reply-content').forEach(el => { // 优先获取内部的 p 标签内容 const p = el.querySelector('p'); if (p) { const text = p.innerText.trim(); if (text) texts.push(text); } else { // 如果没有 p 标签,则获取自身的文本(排除隐藏元素和脚本) const text = el.innerText.trim(); if (text) texts.push(text); } }); // 如果上述方法没找到,降级为查找 .reply-content-container 内的 .reply-content if (!texts.length) { document.querySelectorAll('.reply-content-container .reply-content').forEach(el => { const p = el.querySelector('p'); const text = p ? p.innerText.trim() : el.innerText.trim(); if (text) texts.push(text); }); } return texts.length ? texts.reduce((a, b) => a.length > b.length ? a : b) : ''; } // 检查是否已提交过(编辑按钮可见) hasSubmitted() { const editBtn = document.querySelector('i.post-submit-edit'); if (!editBtn) return false; // 检查是否可见 const isVisible = editBtn.offsetParent !== null && window.getComputedStyle(editBtn).display !== 'none'; return isVisible; }, // 查找可见的“回复”按钮 findReplyBtn() { const candidates = document.querySelectorAll('i.post-submit, i, button, .btn'); for (let el of candidates) { const text = el.innerText.trim(); if ((text === '回复' || text.includes('回复')) && !text.includes('编辑')) { // 必须可见 if (el.offsetParent !== null && window.getComputedStyle(el).display !== 'none') { return el; } } } return null; }, enableButton(btn) { if (!btn) return; btn.classList.remove('disabled', 'btn-disabled', 'ban-click'); btn.style.pointerEvents = 'auto'; btn.style.opacity = '1'; btn.disabled = false; }, async waitForSuccess() { const start = Date.now(); while (Date.now() - start < 8000) { // 检查成功提示 const tip = document.querySelector('.success-tip, .success, .message-success, .ant-message-success'); if (tip && tip.offsetParent !== null) return true; // 检查编辑按钮变为可见 const editBtn = document.querySelector('i.post-submit-edit'); if (editBtn && editBtn.offsetParent !== null && window.getComputedStyle(editBtn).display !== 'none') { return true; } await wait(1000); } return false; } }; const getTopicChapters = () => { return Array.from(document.querySelectorAll('a[content_type="Topic"]')).map(a => ({ chapterId: a.getAttribute('chapter_id'), title: a.querySelector('.title')?.innerText?.trim() || a.innerText.trim() })); }; const switchToChapter = chapterId => { location.hash = location.hash.replace(/chapterId=\d+/, `chapterId=${chapterId}`); }; const topics = getTopicChapters(); if (!topics.length) { console.error('❌ 未找到任何专题,请检查页面'); discussRunning = false; return; } console.log(`🎯 发现 ${topics.length} 个专题`); for (let i = 0; i < topics.length; i++) { const t = topics[i]; console.log(`\n📌 [${i+1}/${topics.length}] 处理专题: ${t.title}`); switchToChapter(t.chapterId); await wait(2000); // 关键修正:检查“编辑”按钮是否可见 if (Editor.hasSubmitted()) { console.warn(' ⏭️ 该专题已提交过(检测到可见的“编辑”按钮),跳过'); continue; } const comment = Editor.getLongestComment(); if (!comment) { console.warn(' ⚠️ 未找到可复制评论,跳过'); continue; } console.log(` 📋 复制评论 (${comment.length}字)`); if (!Editor.setContent(comment)) { console.error(' ❌ 填充内容失败'); continue; } console.log(' ✅ 内容已填入'); await wait(1000); const btn = Editor.findReplyBtn(); if (!btn) { console.warn(' ❌ 未找到可见的“回复”按钮,跳过'); continue; } Editor.enableButton(btn); console.log(` 🚀 点击提交: ${btn.innerText}`); btn.click(); await Editor.waitForSuccess(); await wait(2000); } console.log('%c🎉🎉🎉 所有专题评论完成!刷新页面即可查看结果。 🎉🎉🎉', 'color:green;font-size:20px;background:#f0f0f0;padding:8px;border-radius:4px'); GM_deleteValue(STATE_KEY); discussRunning = false; } // ========== 主控逻辑 ========== (async function main() { const step = GM_getValue(STATE_KEY, 'progress'); console.log(`🔍 状态: ${step} | 课程页: ${isChapterPage} | 讨论页: ${isDiscussPage()}`); // 优先课程页刷进度 if (isChapterPage) { if (step === 'discuss') { console.log('重置状态为进度模式'); GM_setValue(STATE_KEY, 'progress'); } await runProgress(); return; } // 讨论页且状态为 discuss → 直接评论(也可能需要轮询) if (isDiscussPage() && step === 'discuss') { if (!discussRunning) { await runDiscuss(); } return; } // 讨论页但状态为 progress → 直接评论 if (isDiscussPage() && step === 'progress') { console.log('已在讨论页,直接执行评论'); GM_setValue(STATE_KEY, 'discuss'); if (!discussRunning) { await runDiscuss(); } return; } // 其他情况:如果状态是 discuss,启动轮询等待进入讨论区 if (step === 'discuss') { startPollingForDiscuss(); console.log('⏳ 等待进入讨论区...(将自动检测)'); } else { console.log('当前页面无需自动操作'); } })(); })();