Skip Setms Auto

Auto skip Goertek SETMS courses by sending study progress requests

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 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] 脚本已加载');
})();