Auto skip Goertek SETMS courses by sending study progress requests
// ==UserScript==
// @name Skip Setms Auto
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Auto skip Goertek SETMS courses by sending study progress requests
// @author lepecoder
// @match https://setms.goertek.com:8001/page/*
// @grant GM_xmlhttpRequest
// @grant GM_cookie
// @connect setms.goertek.com
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let countdown = 5;
let countdownInterval = null;
let isRunning = false;
// 创建提示信息元素
function createInfoPanel() {
if (document.getElementById('skipSetmsPanel')) return;
const panel = document.createElement('div');
panel.id = 'skipSetmsPanel';
panel.innerHTML = `跳过学习工具已就绪<br><span id="countdownText" style="font-size: 12px;">(${countdown}s 后可点击)</span>`;
panel.style.position = 'fixed';
panel.style.top = '10px';
panel.style.right = '10px';
panel.style.zIndex = '9999';
panel.style.padding = '15px';
panel.style.backgroundColor = '#ffffcc';
panel.style.border = '2px solid #ffcc00';
panel.style.borderRadius = '8px';
panel.style.fontSize = '14px';
panel.style.maxWidth = '350px';
panel.style.cursor = 'not-allowed';
panel.style.opacity = '0.7';
panel.style.fontFamily = 'Arial, sans-serif';
panel.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
panel.addEventListener('click', handleClick);
document.body.appendChild(panel);
// 添加提示
const tip = document.createElement('div');
tip.id = 'tipText';
tip.textContent = '自动跳过 SETMS 学习';
tip.style.fontSize = '12px';
tip.style.marginTop = '8px';
tip.style.color = '#666';
tip.style.fontWeight = 'bold';
panel.appendChild(tip);
// 开始倒计时
startCountdown();
}
// 开始倒计时
function startCountdown() {
countdown = 5;
updateCountdownDisplay();
countdownInterval = setInterval(() => {
countdown--;
updateCountdownDisplay();
if (countdown <= 0) {
clearInterval(countdownInterval);
enablePanel();
}
}, 1000);
}
// 更新倒计时显示
function updateCountdownDisplay() {
const countdownText = document.getElementById('countdownText');
if (countdownText) {
if (countdown > 0) {
countdownText.textContent = `(${countdown}s 后可点击)`;
} else {
countdownText.textContent = '(点击开始跳过)';
}
}
}
// 启用面板点击功能
function enablePanel() {
const panel = document.getElementById('skipSetmsPanel');
if (panel) {
panel.style.cursor = 'pointer';
panel.style.opacity = '1.0';
panel.style.backgroundColor = '#ffff99';
}
}
// 禁用面板点击功能
function disablePanel() {
const panel = document.getElementById('skipSetmsPanel');
if (panel) {
panel.style.cursor = 'not-allowed';
panel.style.opacity = '0.7';
panel.style.backgroundColor = '#ffffcc';
}
}
// 处理点击事件
function handleClick() {
if (countdown > 0 || isRunning) return; // 倒计时期间或运行中不允许点击
skipSetms();
}
// 更新提示信息
function updateInfoPanel(message, subMessage = '', color = '#000000') {
// 确保面板存在
if (!document.getElementById('skipSetmsPanel')) {
createInfoPanel();
}
const panel = document.getElementById('skipSetmsPanel');
const tipText = document.getElementById('tipText');
if (panel) {
panel.firstChild.textContent = message;
panel.style.color = color;
if (tipText && subMessage) {
tipText.textContent = subMessage;
}
}
}
// 从 URL 获取学习参数
function getStudyParamsFromUrl() {
console.log('[SkipSetms] 当前 URL:', window.location.href);
// 从 URL Hash 中获取参数
// URL 格式: https://setms.goertek.com:8001/page/#/trainResIndexEduView?code=ZS00000126031100011&sid=145656&stype=T&ID=145656&VideoPlanMode=A
let hashParams = {};
if (window.location.hash && window.location.hash.includes('?')) {
const hash = window.location.hash;
const queryPart = hash.substring(hash.indexOf('?') + 1);
hashParams = new URLSearchParams(queryPart);
console.log('[SkipSetms] Hash 参数:', Object.fromEntries(hashParams));
}
// 从 URL Query 中获取参数(备选)
const urlParams = new URLSearchParams(window.location.search);
// 优先使用 Hash 参数,其次使用 Query 参数
const code = hashParams.get('code') || urlParams.get('code');
const sid = hashParams.get('sid') || urlParams.get('sid');
const stype = hashParams.get('stype') || urlParams.get('stype');
if (!code || !sid) {
throw new Error('无法从 URL 获取 code 和 sid 参数');
}
const params = {
code: code,
sid: parseInt(sid),
stype: stype || 'C'
};
console.log('[SkipSetms] 从 URL 获取到参数:', params);
return params;
}
// 从本地存储获取 Authorization Token
function getAuthorizationToken() {
console.log('[SkipSetms] ========== 从本地存储获取 Token ==========');
// 方法 1: 从 localStorage.user.userInfo.Token 获取
try {
const userStr = localStorage.getItem('user');
if (userStr) {
const user = JSON.parse(userStr);
if (user && user.userInfo && user.userInfo.Token) {
const token = user.userInfo.Token;
console.log('[SkipSetms] ✓ 从 localStorage.user.userInfo.Token 获取到 Token:', token.substring(0, 20) + '...');
return token;
}
}
} catch (e) {
console.warn('[SkipSetms] 解析 localStorage.user 失败:', e);
}
// 方法 2: 使用 GM_cookie.list() 读取 HttpOnly Cookie
if (typeof GM_cookie !== 'undefined' && typeof GM_cookie.list === 'function') {
console.log('[SkipSetms] 尝试从 GM_cookie.list() 获取 Token...');
try {
const cookies = GM_cookie.list({
url: 'https://setms.goertek.com:8001'
});
console.log(`[SkipSetms] GM_cookie.list() 找到 ${cookies.length} 个 Cookie`);
// 优先查找 .AspNetCore.Session
const aspNetSession = cookies.find(c => c.name === '.AspNetCore.Session');
if (aspNetSession) {
console.log('[SkipSetms] ✓ 从 GM_cookie .AspNetCore.Session 获取到 Token:', aspNetSession.value.substring(0, 20) + '...');
return aspNetSession.value;
}
// 其次查找 satoken
const satoken = cookies.find(c => c.name === 'satoken');
if (satoken) {
console.log('[SkipSetms] ✓ 从 GM_cookie satoken 获取到 Token:', satoken.value.substring(0, 20) + '...');
return satoken.value;
}
} catch (e) {
console.warn('[SkipSetms] GM_cookie.list() 失败:', e);
}
}
console.error('[SkipSetms] 无法从任何来源获取 Token');
console.error('[SkipSetms] localStorage.user:', localStorage.getItem('user'));
throw new Error('无法获取授权 Token');
}
// 从课程详情 API 获取 Guid
function getStudyDetail(code, sid, stype) {
return new Promise((resolve, reject) => {
const token = getAuthorizationToken();
const url = `https://setms.goertek.com:8001/api/v2/EduRes/GetStudyDetail?code=${encodeURIComponent(code)}&sid=${sid}&stype=${stype}`;
console.log('[SkipSetms] 正在获取课程详情...');
console.log('[SkipSetms] 请求 URL:', url);
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Authorization': `Bearer ${token}`
},
onload: function(response) {
console.log('[SkipSetms] 课程详情响应状态:', response.status);
if (response.status === 200) {
try {
const json = JSON.parse(response.responseText);
console.log('[SkipSetms] 课程详情响应:', json);
if (json.success && json.data) {
resolve(json.data);
} else {
reject(new Error('获取课程详情失败'));
}
} catch (e) {
reject(new Error('解析课程详情失败: ' + e.message));
}
} else {
reject(new Error(`获取课程详情失败: HTTP ${response.status}`));
}
},
onerror: function(error) {
reject(new Error('网络请求失败: ' + error));
},
ontimeout: function() {
reject(new Error('请求超时'));
},
timeout: 10000
});
});
}
// 生成 GUID
function generateGuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 获取学习进度(使用 GM_xmlhttpRequest,自动包含 HttpOnly Cookie)
function getStudyProgress(code, sid, stype) {
return new Promise((resolve, reject) => {
const url = `https://setms.goertek.com:8001/api/v2/EduStudyRecord/GetStudyProgress?code=${encodeURIComponent(code)}&sid=${sid}&stype=${stype}`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6'
},
onload: function(response) {
if (response.status === 200) {
try {
const json = JSON.parse(response.responseText);
if (json.success && json.data) {
resolve(json.data);
} else {
reject(new Error('获取学习进度失败'));
}
} catch (e) {
reject(new Error('解析学习进度失败: ' + e.message));
}
} else {
reject(new Error(`获取学习进度失败: HTTP ${response.status}`));
}
},
onerror: function(error) {
reject(new Error('网络请求失败: ' + error));
},
ontimeout: function() {
reject(new Error('请求超时'));
},
timeout: 10000
});
});
}
// 更新学习进度(使用 GM_xmlhttpRequest,自动包含 HttpOnly Cookie)
function updateStudyProgress(code, sid, stype, guid) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({
"Code": code,
"SID": sid,
"SType": stype,
"Guid": guid
});
GM_xmlhttpRequest({
method: 'POST',
url: 'https://setms.goertek.com:8001/api/v2/EduStudyRecord/Study',
headers: {
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Content-Type': 'application/json'
},
data: data,
onload: function(response) {
if (response.status === 200) {
try {
const json = JSON.parse(response.responseText);
if (json.success && json.data) {
resolve(json.data);
} else {
reject(new Error('更新学习进度失败'));
}
} catch (e) {
reject(new Error('解析响应失败: ' + e.message));
}
} else {
reject(new Error(`更新学习进度失败: HTTP ${response.status}`));
}
},
onerror: function(error) {
reject(new Error('网络请求失败: ' + error));
},
ontimeout: function() {
reject(new Error('请求超时'));
},
timeout: 10000
});
});
}
// 延迟函数
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 主要功能函数
async function skipSetms() {
isRunning = true;
disablePanel();
if (countdownInterval) {
clearInterval(countdownInterval);
}
try {
// 步骤 1: 从 URL 获取 code, sid, stype
updateInfoPanel('正在获取学习参数...', '', '#0000ff');
const urlParams = getStudyParamsFromUrl();
await delay(500);
// 步骤 2: 从课程详情 API 获取 Guid
updateInfoPanel('正在获取课程详情...', '', '#0000ff');
const studyDetail = await getStudyDetail(urlParams.code, urlParams.sid, urlParams.stype);
const guid = studyDetail.CurrentGuid || studyDetail.Details?.[0]?.Guid;
if (!guid) {
throw new Error('无法从课程详情获取 Guid');
}
console.log('[SkipSetms] 从课程详情获取到 Guid:', guid);
const params = {
...urlParams,
guid: guid
};
console.log('[SkipSetms] 完整学习参数:', params);
await delay(500);
// 步骤 3: 获取初始学习进度
updateInfoPanel('正在获取初始进度...', '', '#0000ff');
const initialProgress = await getStudyProgress(params.code, params.sid, params.stype);
console.log('[SkipSetms] 初始进度:', initialProgress);
updateInfoPanel(
`当前进度: ${initialProgress.CurrentProgress.toFixed(2)}%`,
`剩余时间: ${initialProgress.LeftStudyHours.toFixed(2)}小时`,
'#0000ff'
);
await delay(500);
// 检查是否已完成
if (initialProgress.CurrentProgress >= 99.9) {
updateInfoPanel('学习已完成!', '无需跳过', '#00aa00');
isRunning = false;
startCountdown();
return;
}
// 循环发送更新请求直到达到 100%
let requestCount = 0;
let currentProgress = initialProgress.CurrentProgress;
const maxRequests = 500; // 最多发送 500 次请求
const requestDelay = 500; // 每次请求间隔 500ms
updateInfoPanel('开始自动跳过...', '', '#ff9900');
while (currentProgress < 99.9 && requestCount < maxRequests) {
try {
requestCount++;
updateInfoPanel(
`正在跳过... (${requestCount})`,
`进度: ${currentProgress.toFixed(2)}%`,
'#ff9900'
);
const result = await updateStudyProgress(params.code, params.sid, params.stype, params.guid);
console.log(`[SkipSetms] 请求 ${requestCount} 结果:`, result);
currentProgress = result.CurrentProgress || currentProgress;
const leftHours = result.LeftStudyHours || 0;
// 每 10 次请求显示一次详细信息
if (requestCount % 10 === 0) {
console.log(`[SkipSetms] 当前进度: ${currentProgress.toFixed(2)}%, 剩余时间: ${leftHours.toFixed(2)}小时`);
}
// 延迟
await delay(requestDelay);
} catch (error) {
console.error(`[SkipSetms] 请求 ${requestCount} 失败:`, error);
// 失败后继续尝试
await delay(1000);
}
}
// 最终检查
updateInfoPanel('正在验证最终进度...', '', '#0000ff');
const finalProgress = await getStudyProgress(params.code, params.sid, params.stype);
console.log('[SkipSetms] 最终进度:', finalProgress);
if (finalProgress.CurrentProgress >= 99.9) {
updateInfoPanel(
'跳过完成!正在刷新页面...',
`最终进度: ${finalProgress.CurrentProgress.toFixed(2)}%`,
'#00aa00'
);
console.log('[SkipSetms] 跳过完成,将在 2 秒后刷新页面');
setTimeout(() => {
location.reload();
}, 2000);
} else {
updateInfoPanel(
'跳过完成,正在刷新页面...',
`最终进度: ${finalProgress.CurrentProgress.toFixed(2)}%`,
'#ff9900'
);
console.log('[SkipSetms] 跳过完成,将在 2 秒后刷新页面');
setTimeout(() => {
location.reload();
}, 2000);
}
} catch (error) {
console.error('[SkipSetms] 错误:', error);
updateInfoPanel('跳过失败,正在刷新页面...', error.message, '#ff0000');
console.log('[SkipSetms] 跳过失败,将在 3 秒后刷新页面');
setTimeout(() => {
location.reload();
}, 3000);
}
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createInfoPanel);
} else {
createInfoPanel();
}
console.log('[SkipSetms] 脚本已加载');
})();