Skip Setms Auto

Auto skip Goertek SETMS courses by sending study progress requests

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==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] 脚本已加载');
})();