// ==UserScript== // @name 中国农业大学实验室安全 - 课程与答题自动完成 // @namespace https://sysks.cau.edu.cn/ // @version 3.0.0 // @description 动态解析知识树接口获取全部课件,自动答题、上报进度并完成 // @author Assistant // @match https://sysks.cau.edu.cn/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // 获取后端 API 统一前缀 const BASE_URL = (window._CONFIG && window._CONFIG.domianURL) || '/api/jeecg-boot'; // 1. 自动提取登录凭证 Token function getAccessToken() { try { const raw = localStorage.getItem('pro__Access-Token'); if (raw) { const parsed = JSON.parse(raw); if (parsed && parsed.value) return parsed.value; } } catch (e) {} return ( localStorage.getItem('x-access-token') || localStorage.getItem('token') || sessionStorage.getItem('x-access-token') || '' ); } // 2. 通用网络请求封装 async function requestApi(url, method = 'GET', data = null, token) { const headers = { 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json;charset=UTF-8', 'x-access-token': token, 'tenant-id': '0' }; const options = { method, headers }; if (data && method === 'POST') { options.body = JSON.stringify(data); } const resp = await fetch(url, options); return await resp.json(); } // 3. 递归扁平化知识树,提取所有叶子课件及 graphId function extractCoursesFromTree(treeNodes, defaultGraphId = '') { let courses = []; for (const node of treeNodes) { // 树的第一层节点 key 即为 graphId const currentGraphId = defaultGraphId || node.key; if (node.isLeaf === true || (!node.children && node.label === '1')) { // 叶子课件节点 courses.push({ id: node.key, title: node.title, graphId: currentGraphId }); } else if (node.children && Array.isArray(node.children)) { // 非叶子分类节点,向下递归 courses = courses.concat(extractCoursesFromTree(node.children, currentGraphId)); } } return courses; } // 4. 解析题目答案 function resolveQuestionAnswer(q) { // 优先读取后台直接返回的答案字段 const answer = q.answer || q.rightAnswer || q.correctAnswer || q.rightOption || q.daan || q.rightKey; // 多选题 (kind === "3") if (q.kind === '3') { if (Array.isArray(answer)) return answer; if (typeof answer === 'string' && answer.length > 0) { return answer.includes(',') ? answer.split(',') : answer.split(''); } return ['A', 'B', 'C', 'D']; // 默认全选 } // 单选题 / 判断题 (kind === "1" 或 "2") if (typeof answer === 'string' && answer.trim().length > 0) { return answer.trim(); } return 'A'; // 默认选 A } // 5. 核心自动化流程 async function startAutoProcess(btn) { const token = getAccessToken(); if (!token) { alert('❌ 未能读取到登录 Token,请先登录平台!'); return; } btn.disabled = true; btn.style.cursor = 'not-allowed'; btn.style.backgroundColor = '#faad14'; btn.innerText = '🔍 正在拉取课程树...'; try { // 步骤 1:调用知识树接口 const treeApiUrl = `${BASE_URL}/jcedutec/knowledgeGraph/queryGraphTree?_t=${Date.now()}`; console.log(`[1/4] 发起知识树请求: ${treeApiUrl}`); const treeRes = await requestApi(treeApiUrl, 'GET', null, token); if (!treeRes.success || !treeRes.result || treeRes.result.length === 0) { throw new Error(treeRes.message || '获取课程树为空,请确认是否已加入课程!'); } // 步骤 2:扁平化提取全部课件 const courseList = extractCoursesFromTree(treeRes.result); console.log(`[2/4] 解析完成,共发现 ${courseList.length} 个课件:`, courseList); if (courseList.length === 0) { alert('未解析到任何课件,请检查课程树数据。'); btn.disabled = false; btn.innerText = '⚡ 一键动态刷课'; btn.style.backgroundColor = '#722ed1'; return; } if (!confirm(`成功拉取到 ${courseList.length} 门课件!\n是否开始自动答题并标记完成?`)) { btn.disabled = false; btn.innerText = '⚡ 一键动态刷课'; btn.style.backgroundColor = '#722ed1'; return; } // 步骤 3:按顺序处理每一门课程 let successCount = 0; for (let i = 0; i < courseList.length; i++) { const item = courseList[i]; btn.innerText = `⏳ (${i + 1}/${courseList.length}) ${item.title.slice(0, 7)}...`; console.log(`\n==================================================`); console.log(`▶ [${i + 1}/${courseList.length}] 正在处理: ${item.title} (ID: ${item.id})`); try { // 3.1 拉取视频内绑定的题目 const questionUrl = `${BASE_URL}/jcedutec/courseSource/queryCourseQuestionRelaByMainId?id=${item.id}&graphId=${item.graphId}`; const qRes = await requestApi(questionUrl, 'GET', null, token); const questions = qRes.result || []; if (questions.length > 0) { console.log(` 📝 找到 ${questions.length} 道习题,开始作答...`); for (const q of questions) { const myAnswer = resolveQuestionAnswer(q); const submitPayload = { id: item.id, graphId: item.graphId, questionId: q.id, option: myAnswer }; const ansRes = await requestApi(`${BASE_URL}/jcedutec/courseSource/submitAnswer`, 'POST', submitPayload, token); console.log(` ↳ 题干: "${q.stem?.slice(0, 15)}..." | 答案: ${JSON.stringify(myAnswer)} | 结果:`, ansRes.message || 'OK'); await new Promise(r => setTimeout(r, 250)); } } else { console.log(` ℹ️ 该课件无内置习题`); } // 3.2 上报观看进度(设置时长防校验) await requestApi(`${BASE_URL}/jcedutec/courseSource/finishRate`, 'POST', { id: item.id, graphId: item.graphId, watchDuration: 3600 }, token); // 3.3 提交最终完成状态 const finishRes = await requestApi(`${BASE_URL}/jcedutec/courseSource/finish`, 'POST', { id: item.id, sourceId: item.id, graphId: item.graphId }, token); if (finishRes.success || finishRes.code === 200) { successCount++; console.log(` ✅ 课件完成成功`); } else { console.warn(` ⚠️ 课件完成接口异常:`, finishRes); } } catch (subErr) { console.error(` ❌ 课件处理出错: ${item.title}`, subErr); } // 单课处理间隔 1 秒 await new Promise(r => setTimeout(r, 1000)); } // 步骤 4:全部完成 btn.innerText = `🎉 已完成 (${successCount}/${courseList.length})`; btn.style.backgroundColor = '#52c41a'; alert(`🎉 恭喜!已完成全部 ${successCount} 个课件的答题与学习,请刷新网页查看!`); } catch (err) { console.error('任务执行失败:', err); alert('执行失败: ' + err.message); btn.disabled = false; btn.innerText = '⚡ 重试刷课'; btn.style.backgroundColor = '#f5222d'; } } // 6. 创建界面悬浮按钮 function injectUI() { if (document.getElementById('cau-dynamic-study-btn')) return; const btn = document.createElement('button'); btn.id = 'cau-dynamic-study-btn'; btn.innerText = '⚡ 一键动态刷课'; Object.assign(btn.style, { position: 'fixed', bottom: '30px', right: '30px', zIndex: '999999', padding: '12px 20px', backgroundColor: '#722ed1', color: '#ffffff', border: 'none', borderRadius: '8px', boxShadow: '0 4px 14px rgba(114, 46, 209, 0.4)', cursor: 'pointer', fontSize: '14px', fontWeight: 'bold', transition: 'all 0.3s' }); btn.onmouseover = () => { if (!btn.disabled) btn.style.backgroundColor = '#9254de'; }; btn.onmouseout = () => { if (!btn.disabled) btn.style.backgroundColor = '#722ed1'; }; btn.onclick = () => startAutoProcess(btn); document.body.appendChild(btn); } window.addEventListener('load', injectUI); setTimeout(injectUI, 1200); })();