Skip to content

使用你的项目然后用Ai vibe的油猴脚本 这可以减少人们下载python等复杂步骤 推荐您在您的项目中发布它 #96

Description

@yueseqaz
// ==UserScript==
// @name         Sakura安全微伴
// @version      1.0.0
// @description  2026最新安全微伴答题脚本,自动学习+自动考试,支持本地题库和用户上传题库
// @author       Sakura
// @match        https://*.mycourse.cn/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_notification
// @grant        GM_registerMenuCommand
// @connect      cdn.jsdelivr.net
// @connect      ghfast.top
// @connect      github.com
// @connect      weiban.mycourse.cn
// ==/UserScript==

(function () {
    'use strict';

    // ==================== 配置 ====================
    const CONFIG = {
        studyTime: 15,           // 每门课程学习时间(秒)
        examUseTime: 250,        // 考试总用时(秒)
        answerUrl: 'https://cdn.jsdelivr.net/gh/hangone/WeBan@main/answer/answer.json',
        localAnswerKey: 'weban_answers'
    };

    // ==================== 登录状态管理 ====================
    
    // 从网站获取登录信息
    function getLoginInfo() {
        const info = {
            token: '',
            userId: '',
            tenantCode: '',
            userName: '',
            realName: '',
            tenantName: ''
        };

        try {
            // 方式1: 从 localStorage 获取
            const token = localStorage.getItem('token');
            const userStr = localStorage.getItem('user');
            
            if (token) {
                info.token = token;
            }
            
            if (userStr) {
                const user = JSON.parse(userStr);
                info.userId = user.userId || '';
                info.tenantCode = user.tenantCode || '';
                info.userName = user.userName || '';
                info.realName = user.realName || '';
                info.tenantName = user.tenantName || '';
                if (user.token) {
                    info.token = user.token;
                }
            }

            // 方式2: 从 sessionStorage 获取
            if (!info.token) {
                const sessionToken = sessionStorage.getItem('token');
                if (sessionToken) {
                    info.token = sessionToken;
                }
            }
            
            if (!info.userId) {
                const sessionUser = sessionStorage.getItem('user');
                if (sessionUser) {
                    const user = JSON.parse(sessionUser);
                    info.userId = user.userId || '';
                    info.tenantCode = user.tenantCode || '';
                    info.userName = user.userName || '';
                    info.realName = user.realName || '';
                    info.tenantName = user.tenantName || '';
                }
            }

            // 方式3: 从 URL 参数获取
            const urlParams = new URLSearchParams(window.location.search);
            if (!info.token) {
                info.token = urlParams.get('token') || '';
            }
            if (!info.tenantCode) {
                info.tenantCode = urlParams.get('tenantCode') || '';
            }

            // 方式4: 从 Cookie 获取
            if (!info.token) {
                const cookies = document.cookie.split(';');
                for (const cookie of cookies) {
                    const [name, value] = cookie.trim().split('=');
                    if (name === 'token' || name === 'X-Token') {
                        info.token = value;
                    }
                    if (name === 'tenantCode') {
                        info.tenantCode = value;
                    }
                }
            }

            // 方式5: 从 Vue/React 应用状态获取
            if (!info.token && window.__INITIAL_STATE__) {
                const state = window.__INITIAL_STATE__;
                info.token = state.token || state.user?.token || '';
                info.userId = state.userId || state.user?.userId || '';
            }

            // 方式6: 从 Vuex store 获取
            if (!info.token && window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
                try {
                    const stores = Object.values(window.__VUE_DEVTOOLS_GLOBAL_HOOK__.stores || {});
                    for (const store of stores) {
                        if (store.token) info.token = store.token;
                        if (store.userId) info.userId = store.userId;
                        if (store.user) {
                            info.token = store.user.token || info.token;
                            info.userId = store.user.userId || info.userId;
                        }
                    }
                } catch (e) {}
            }

        } catch (e) {
            console.error('[登录状态] 获取登录信息失败:', e);
        }

        return info;
    }

    // 检查是否已登录
    function isLoggedIn() {
        const info = getLoginInfo();
        return !!(info.token && info.userId);
    }

    // 获取请求头
    function getHeaders() {
        const info = getLoginInfo();
        return {
            'Content-Type': 'application/x-www-form-urlencoded',
            'X-Token': info.token || '',
            'Accept': 'application/json, text/plain, */*',
        };
    }

    // ==================== 工具函数 ====================
    
    // 清理文本(只保留字母、数字和汉字)
    function cleanText(text) {
        return text.replace(/[^\w\u4e00-\u9fa5]/g, '');
    }

    // 获取时间戳
    function getTimestamp() {
        return Date.now().toString();
    }

    // 延迟函数
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // ==================== 题库管理 ====================
    
    // 加载本地题库
    function loadLocalAnswers() {
        try {
            const data = GM_getValue(CONFIG.localAnswerKey);
            return data ? JSON.parse(data) : {};
        } catch (e) {
            console.error('[题库] 加载本地题库失败:', e);
            return {};
        }
    }

    // 保存题库到本地
    function saveLocalAnswers(answers) {
        try {
            GM_setValue(CONFIG.localAnswerKey, JSON.stringify(answers));
            console.log('[题库] 题库已保存');
        } catch (e) {
            console.error('[题库] 保存题库失败:', e);
        }
    }

    // 下载题库
    function downloadAnswers() {
        return new Promise((resolve, reject) => {
            console.log('[题库] 正在下载题库...');
            GM_xmlhttpRequest({
                method: 'GET',
                url: CONFIG.answerUrl,
                onload: function(response) {
                    try {
                        const answers = JSON.parse(response.responseText);
                        saveLocalAnswers(answers);
                        console.log('[题库] 题库下载成功,共', Object.keys(answers).length, '道题目');
                        resolve(answers);
                    } catch (e) {
                        console.error('[题库] 解析题库失败:', e);
                        reject(e);
                    }
                },
                onerror: function(error) {
                    console.error('[题库] 下载题库失败:', error);
                    reject(error);
                }
            });
        });
    }

    // 获取题库(优先本地,否则下载)
    async function getAnswers() {
        let answers = loadLocalAnswers();
        if (Object.keys(answers).length === 0) {
            try {
                answers = await downloadAnswers();
            } catch (e) {
                console.error('[题库] 获取题库失败,使用空题库');
            }
        }
        return answers;
    }

    // 构建答案索引(题目 -> 正确答案列表)
    function buildAnswerIndex(answers) {
        const index = {};
        for (const [title, data] of Object.entries(answers)) {
            const cleanTitle = cleanText(title);
            if (!index[cleanTitle]) {
                index[cleanTitle] = [];
            }
            const correctOptions = data.optionList
                .filter(opt => opt.isCorrect === 1)
                .map(opt => cleanText(opt.content));
            index[cleanTitle].push(...correctOptions);
        }
        return index;
    }

    // 合并题库
    function mergeAnswers(oldAnswers, newAnswers) {
        const merged = { ...oldAnswers };
        for (const [title, data] of Object.entries(newAnswers)) {
            if (!merged[title]) {
                merged[title] = data;
            } else {
                // 合并选项
                const existingContents = new Set(merged[title].optionList.map(o => o.content));
                for (const opt of data.optionList) {
                    if (!existingContents.has(opt.content)) {
                        merged[title].optionList.push(opt);
                    }
                }
            }
        }
        return merged;
    }

    // ==================== API 封装 ====================
    
    // POST 请求
    async function postRequest(url, data) {
        const info = getLoginInfo();
        const formData = new URLSearchParams();
        formData.append('tenantCode', info.tenantCode);
        formData.append('userId', info.userId);
        for (const [key, value] of Object.entries(data)) {
            if (value !== undefined && value !== null) {
                formData.append(key, value);
            }
        }
        formData.append('timestamp', getTimestamp());

        const response = await fetch(url, {
            method: 'POST',
            headers: getHeaders(),
            body: formData.toString()
        });
        return response.json();
    }

    // GET 请求
    async function getRequest(url, params = {}) {
        const info = getLoginInfo();
        const searchParams = new URLSearchParams();
        searchParams.append('tenantCode', info.tenantCode);
        searchParams.append('userId', info.userId);
        for (const [key, value] of Object.entries(params)) {
            if (value !== undefined && value !== null) {
                searchParams.append(key, value);
            }
        }
        searchParams.append('timestamp', getTimestamp());

        const response = await fetch(`${url}?${searchParams.toString()}`, {
            method: 'GET',
            headers: getHeaders()
        });
        return response.json();
    }

    // ==================== 学习功能 ====================
    
    // 获取学习任务列表
    async function getStudyTasks() {
        const result = await postRequest('https://weiban.mycourse.cn/pharos/index/listStudyTask.do', {});
        return result.data?.studyTaskList || [];
    }

    // 获取课程分类
    async function getCategories(userProjectId, chooseType) {
        const result = await postRequest('https://weiban.mycourse.cn/pharos/usercourse/listCategory.do', {
            userProjectId,
            chooseType
        });
        return result.data || [];
    }

    // 获取课程列表
    async function getCourses(userProjectId, categoryCode, chooseType) {
        const result = await postRequest('https://weiban.mycourse.cn/pharos/usercourse/listCourse.do', {
            userProjectId,
            categoryCode,
            chooseType
        });
        return result.data || [];
    }

    // 开始学习课程
    async function studyCourse(courseId, userProjectId) {
        return postRequest('https://weiban.mycourse.cn/pharos/usercourse/study.do', {
            courseId,
            userProjectId
        });
    }

    // 获取课程链接
    async function getCourseUrl(courseId, userProjectId) {
        const result = await postRequest('https://weiban.mycourse.cn/pharos/usercourse/getCourseUrl.do', {
            courseId,
            userProjectId
        });
        return result.data || '';
    }

    // 完成课程
    async function finishCourse(userCourseId, token = null) {
        const info = getLoginInfo();
        const url = token 
            ? `https://weiban.mycourse.cn/pharos/usercourse/v2/${token}.do`
            : `https://weiban.mycourse.cn/pharos/usercourse/v2/${userCourseId}.do`;
        
        const params = new URLSearchParams();
        params.append('userCourseId', userCourseId);
        params.append('tenantCode', info.tenantCode);
        params.append('_', getTimestamp());
        
        const response = await fetch(`${url}?${params.toString()}`, {
            method: 'GET',
            headers: getHeaders()
        });
        return response.text();
    }

    // 自动学习一门课程
    async function autoStudyCourse(course, userProjectId) {
        console.log(`[学习] 开始学习: ${course.resourceName}`);
        
        // 开始学习
        await studyCourse(course.resourceId, userProjectId);
        
        if (!course.userCourseId) {
            console.log(`[学习] 课程无需等待: ${course.resourceName}`);
            return true;
        }

        // 获取课程链接
        const courseUrl = await getCourseUrl(course.resourceId, userProjectId);
        
        // 检查是否需要验证码
        if (courseUrl.includes('csCapt=true')) {
            console.warn(`[学习] 课程需要验证码,跳过: ${course.resourceName}`);
            return false;
        }

        // 等待学习时间
        console.log(`[学习] 等待 ${CONFIG.studyTime} 秒...`);
        await sleep(CONFIG.studyTime * 1000);

        // 完成课程
        const result = await finishCourse(course.userCourseId);
        console.log(`[学习] 完成: ${course.resourceName}`, result);
        
        return true;
    }

    // 自动学习所有课程
    async function autoStudyAll() {
        if (!isLoggedIn()) {
            alert('请先登录!');
            return;
        }

        console.log('[学习] 开始自动学习...');
        log('开始自动学习...');
        
        const tasks = await getStudyTasks();
        console.log(`[学习] 获取到 ${tasks.length} 个学习任务`);
        log(`获取到 ${tasks.length} 个学习任务`);

        for (const task of tasks) {
            console.log(`[学习] 处理任务: ${task.projectName}`);
            log(`处理任务: ${task.projectName}`);
            
            // 必修课(3)、推送课(1)、自选课(2)
            for (const chooseType of [3, 1, 2]) {
                const typeNames = { 1: '推送课', 2: '自选课', 3: '必修课' };
                const categories = await getCategories(task.userProjectId, chooseType);
                
                for (const category of categories) {
                    if (category.finishedNum >= category.totalNum) {
                        console.log(`[学习] ${typeNames[chooseType]}/${category.categoryName} 已完成`);
                        continue;
                    }

                    console.log(`[学习] 处理分类: ${typeNames[chooseType]}/${category.categoryName}`);
                    log(`处理: ${typeNames[chooseType]}/${category.categoryName}`);
                    const courses = await getCourses(task.userProjectId, category.categoryCode, chooseType);
                    
                    for (const course of courses) {
                        if (course.finished === 1) {
                            console.log(`[学习] 课程已完成: ${course.resourceName}`);
                            continue;
                        }

                        await autoStudyCourse(course, task.userProjectId);
                    }
                }
            }
        }

        console.log('[学习] 自动学习完成');
        log('自动学习完成!');
        GM_notification({ title: '学习完成', text: '所有课程学习完成!' });
    }

    // ==================== 考试功能 ====================
    
    // 获取我的项目列表(用于考试)
    async function getMyProjects() {
        const result = await postRequest('https://weiban.mycourse.cn/pharos/index/listMyProject.do', { ended: 2 });
        return result.data || [];
    }

    // 获取模块完成情况
    async function listCompletion() {
        const result = await postRequest('https://weiban.mycourse.cn/pharos/index/listCompletion.do', {});
        return result.data || [];
    }

    // 获取实验室项目
    async function getLabProject() {
        const result = await postRequest('https://weiban.mycourse.cn/pharos/lab/index.do', {});
        return result.data?.current || null;
    }

    // 获取考试计划
    async function getExamPlans(userProjectId) {
        const result = await postRequest('https://weiban.mycourse.cn/pharos/exam/listPlan.do', {
            userProjectId
        });
        return result;
    }

    // 考试前检查
    async function examBeforePaper(userExamPlanId) {
        return postRequest('https://weiban.mycourse.cn/pharos/exam/beforePaper.do', {
            userExamPlanId
        });
    }

    // 准备考试
    async function prepareExam(userExamPlanId) {
        return postRequest('https://weiban.mycourse.cn/pharos/exam/preparePaper.do', {
            userExamPlanId
        });
    }

    // 开始考试(获取题目)
    async function startExam(userExamPlanId) {
        return postRequest('https://weiban.mycourse.cn/pharos/exam/startPaper.do', {
            userExamPlanId
        });
    }

    // 记录答案
    async function recordAnswer(userExamPlanId, questionId, useTime, answerIds, examPlanId) {
        return postRequest('https://weiban.mycourse.cn/pharos/exam/recordQuestion.do', {
            userExamPlanId,
            questionId,
            useTime,
            answerIds: answerIds.join(','),
            examPlanId
        });
    }

    // 提交试卷
    async function submitExam(userExamPlanId) {
        return postRequest('https://weiban.mycourse.cn/pharos/exam/submitPaper.do', {
            userExamPlanId
        });
    }

    // 自动考试
    async function autoExam() {
        if (!isLoggedIn()) {
            alert('请先登录!');
            return;
        }

        console.log('[考试] 开始自动考试...');
        log('开始自动考试...');
        
        // 加载题库
        const answers = await getAnswers();
        const answerIndex = buildAnswerIndex(answers);
        console.log(`[考试] 题库加载完成,共 ${Object.keys(answerIndex).length} 道题目`);
        log(`题库加载完成,共 ${Object.keys(answerIndex).length} 道题目`);

        // 获取项目列表
        let projects = await getMyProjects();
        console.log(`[考试] 获取到 ${projects.length} 个项目`);
        
        // 检查是否有实验室模块
        const completion = await listCompletion();
        const showableModules = completion.filter(d => d.showable === 1).map(d => d.module);
        
        if (showableModules.includes('labProject')) {
            console.log('[考试] 加载实验室课程');
            const labProject = await getLabProject();
            if (labProject) {
                projects.push(labProject);
            }
        }

        for (const project of projects) {
            if (!project.userProjectId) continue;
            
            console.log(`[考试] 处理项目: ${project.projectName}`);
            log(`处理项目: ${project.projectName}`);
            
            const examPlansResult = await getExamPlans(project.userProjectId);
            if (examPlansResult.code !== '0') {
                console.error(`[考试] 获取考试计划失败:`, examPlansResult);
                continue;
            }
            
            const plans = examPlansResult.data || [];
            
            for (const plan of plans) {
                console.log(`[考试] 处理考试: ${plan.examPlanName}`);
                log(`处理考试: ${plan.examPlanName}`);
                
                const userExamPlanId = plan.id;
                const examPlanId = plan.examPlanId;

                // 检查是否已考试
                if (plan.examFinishNum > 0) {
                    console.log(`[考试] 已考试 ${plan.examFinishNum} 次,最高成绩 ${plan.examScore} 分`);
                    log(`已考试 ${plan.examFinishNum} 次,最高 ${plan.examScore} 分`);
                    // 继续考试,不跳过
                }

                // 考试前检查
                await examBeforePaper(userExamPlanId);

                // 准备考试
                const prepareInfo = await prepareExam(userExamPlanId);
                if (prepareInfo.code !== '0') {
                    console.error(`[考试] 准备考试失败:`, prepareInfo);
                    log(`准备考试失败: ${prepareInfo.msg || JSON.stringify(prepareInfo)}`);
                    continue;
                }

                const questionNum = prepareInfo.data?.questionNum || 0;
                const perTime = Math.max(1, Math.floor(CONFIG.examUseTime / questionNum));
                console.log(`[考试] 题目数: ${questionNum}, 每题用时: ${perTime}秒`);
                log(`题目数: ${questionNum}, 每题用时: ${perTime}秒`);

                // 开始考试
                const examResult = await startExam(userExamPlanId);
                if (examResult.code !== '0') {
                    console.error(`[考试] 获取题目失败:`, examResult);
                    log(`获取题目失败: ${examResult.msg || examResult.detailCode || JSON.stringify(examResult)}`);
                    if (examResult.detailCode === '10018') {
                        log('需要手动在网站上开启一次考试后重试');
                    }
                    continue;
                }

                const examData = examResult.data || {};
                const questions = examData.questionList || [];
                
                if (questions.length === 0) {
                    console.error(`[考试] 题目列表为空`);
                    log('题目列表为空');
                    continue;
                }

                let haveAnswer = 0;
                let noAnswer = 0;

                // 答题
                for (let i = 0; i < questions.length; i++) {
                    const question = questions[i];
                    const cleanTitle = cleanText(question.title);
                    const correctAnswers = answerIndex[cleanTitle];

                    if (correctAnswers && correctAnswers.length > 0) {
                        haveAnswer++;
                        // 匹配答案
                        const answerIds = question.optionList
                            .filter(opt => correctAnswers.includes(cleanText(opt.content)))
                            .map(opt => opt.id);

                        console.log(`[考试] [${i + 1}/${questions.length}] 有答案: ${question.title.substring(0, 30)}...`);
                        log(`[${i + 1}/${questions.length}] 有答案`);
                        
                        await sleep(perTime * 1000);
                        const recordResult = await recordAnswer(userExamPlanId, question.id, perTime + 1, answerIds, examPlanId);
                        if (recordResult.code !== '0') {
                            console.error(`[考试] 记录答案失败:`, recordResult);
                        }
                    } else {
                        noAnswer++;
                        console.warn(`[考试] [${i + 1}/${questions.length}] 无答案: ${question.title.substring(0, 30)}...`);
                        log(`[${i + 1}/${questions.length}] 无答案(随机选择)`);
                        
                        // 随机选择一个答案
                        const randomOption = question.optionList[Math.floor(Math.random() * question.optionList.length)];
                        await sleep(perTime * 1000);
                        const recordResult = await recordAnswer(userExamPlanId, question.id, perTime + 1, [randomOption.id], examPlanId);
                        if (recordResult.code !== '0') {
                            console.error(`[考试] 记录答案失败:`, recordResult);
                        }
                    }
                }

                console.log(`[考试] 答题完成,有答案: ${haveAnswer}, 无答案: ${noAnswer}`);
                log(`答题完成,有答案: ${haveAnswer}, 无答案: ${noAnswer}`);

                // 提交试卷
                const submitResult = await submitExam(userExamPlanId);
                if (submitResult.code === '0') {
                    const score = submitResult.data?.score;
                    console.log(`[考试] 提交成功,成绩: ${score}分`);
                    log(`提交成功,成绩: ${score}分`);
                    GM_notification({ 
                        title: '考试完成', 
                        text: `${project.projectName} - ${plan.examPlanName}\n成绩: ${score}分` 
                    });
                } else {
                    console.error(`[考试] 提交失败:`, submitResult);
                    log(`提交失败: ${submitResult.msg || JSON.stringify(submitResult)}`);
                }
            }
        }

        console.log('[考试] 自动考试完成');
        log('自动考试完成!');
    }

    // ==================== 题库上传功能 ====================
    
    // 创建上传题库的文件输入
    function createUploadInput() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.style.display = 'none';
        input.id = 'weban-answer-upload';
        
        input.onchange = async (e) => {
            const file = e.target.files[0];
            if (!file) return;

            try {
                const text = await file.text();
                const newAnswers = JSON.parse(text);
                
                if (typeof newAnswers !== 'object') {
                    throw new Error('题库格式错误');
                }

                // 加载现有题库并合并
                const oldAnswers = loadLocalAnswers();
                const mergedAnswers = mergeAnswers(oldAnswers, newAnswers);
                
                // 保存合并后的题库
                saveLocalAnswers(mergedAnswers);
                
                const oldCount = Object.keys(oldAnswers).length;
                const newCount = Object.keys(newAnswers).length;
                const mergedCount = Object.keys(mergedAnswers).length;
                
                console.log(`[题库] 上传成功,原有 ${oldCount} 题,新增 ${newCount} 题,合并后 ${mergedCount} 题`);
                log(`题库上传成功!原有 ${oldCount} 题,新增 ${newCount} 题,合并后 ${mergedCount} 题`);
                alert(`题库上传成功!\n原有: ${oldCount} 题\n新增: ${newCount} 题\n合并后: ${mergedCount} 题`);
                
            } catch (err) {
                console.error('[题库] 上传失败:', err);
                log(`题库上传失败: ${err.message}`);
                alert('题库上传失败: ' + err.message);
            }
            
            // 清理 input 以便重复上传
            input.value = '';
        };
        
        document.body.appendChild(input);
        return input;
    }

    // 触发上传
    function triggerUpload() {
        let input = document.getElementById('weban-answer-upload');
        if (!input) {
            input = createUploadInput();
        }
        input.click();
    }

    // 导出题库
    function exportAnswers() {
        const answers = loadLocalAnswers();
        const count = Object.keys(answers).length;
        
        if (count === 0) {
            alert('题库为空,请先下载或上传题库');
            return;
        }

        const blob = new Blob([JSON.stringify(answers, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `weban_answers_${new Date().toISOString().slice(0, 10)}.json`;
        a.click();
        URL.revokeObjectURL(url);
        
        log(`题库已导出,共 ${count} 题`);
    }

    // ==================== UI 控制 ====================
    
    // 日志输出
    function log(msg) {
        console.log('[安全微伴]', msg);
        const logEl = document.getElementById('weban-log');
        if (logEl) {
            logEl.style.display = 'block';
            const time = new Date().toLocaleTimeString();
            logEl.innerHTML += `<div>[${time}] ${msg}</div>`;
            logEl.scrollTop = logEl.scrollHeight;
        }
    }

    // 更新登录状态显示
    function updateLoginStatus() {
        const info = getLoginInfo();
        const statusEl = document.getElementById('weban-login-status');
        if (statusEl) {
            if (info.token && info.userId) {
                statusEl.innerHTML = `<span style="color: #52c41a;">✓ 已登录</span><br><small>${info.realName || info.userName || ''} (${info.tenantName || ''})</small>`;
            } else {
                statusEl.innerHTML = '<span style="color: #ff4d4f;">✗ 未登录</span>';
            }
        }
    }

    // 创建控制面板
    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'weban-control-panel';
        panel.innerHTML = `
            <style>
                #weban-control-panel {
                    position: fixed;
                    top: 10px;
                    right: 10px;
                    z-index: 99999;
                    background: #fff;
                    border: 1px solid #ddd;
                    border-radius: 8px;
                    padding: 15px;
                    box-shadow: 0 2px 12px rgba(0,0,0,0.15);
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                    min-width: 220px;
                }
                #weban-control-panel h3 {
                    margin: 0 0 10px 0;
                    font-size: 14px;
                    color: #333;
                    border-bottom: 1px solid #eee;
                    padding-bottom: 8px;
                }
                #weban-control-panel .status {
                    background: #f5f5f5;
                    padding: 8px;
                    border-radius: 4px;
                    margin-bottom: 10px;
                    font-size: 12px;
                }
                #weban-control-panel button {
                    display: block;
                    width: 100%;
                    margin: 5px 0;
                    padding: 8px 12px;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-size: 13px;
                    transition: all 0.2s;
                }
                #weban-control-panel .btn-study {
                    background: #1890ff;
                    color: white;
                }
                #weban-control-panel .btn-exam {
                    background: #52c41a;
                    color: white;
                }
                #weban-control-panel .btn-download {
                    background: #faad14;
                    color: white;
                }
                #weban-control-panel .btn-upload {
                    background: #722ed1;
                    color: white;
                }
                #weban-control-panel .btn-export {
                    background: #13c2c2;
                    color: white;
                }
                #weban-control-panel .btn-close {
                    background: #f5f5f5;
                    color: #666;
                }
                #weban-control-panel button:hover {
                    opacity: 0.85;
                    transform: translateY(-1px);
                }
                #weban-control-panel button:disabled {
                    opacity: 0.5;
                    cursor: not-allowed;
                }
                #weban-control-panel .log {
                    margin-top: 10px;
                    max-height: 150px;
                    overflow-y: auto;
                    font-size: 11px;
                    color: #666;
                    background: #f9f9f9;
                    padding: 8px;
                    border-radius: 4px;
                    display: none;
                }
                #weban-control-panel .btn-group {
                    display: flex;
                    gap: 5px;
                }
                #weban-control-panel .btn-group button {
                    flex: 1;
                }
            </style>
            <h3>🎓 Sakura安全微伴助手</h3>
            <div class="status" id="weban-login-status">检测登录状态中...</div>
            <button class="btn-study" id="btn-auto-study">📚 自动学习</button>
            <button class="btn-exam" id="btn-auto-exam">📝 自动考试</button>
            <div class="btn-group">
                <button class="btn-download" id="btn-download-answers">⬇️ 下载题库</button>
                <button class="btn-upload" id="btn-upload-answers">⬆️ 上传题库</button>
            </div>
            <button class="btn-export" id="btn-export-answers">📤 导出题库</button>
            <button class="btn-close" id="btn-close-panel">✕ 关闭</button>
            <div class="log" id="weban-log"></div>
        `;
        document.body.appendChild(panel);

        // 绑定事件
        document.getElementById('btn-auto-study').onclick = async () => {
            const btn = document.getElementById('btn-auto-study');
            btn.disabled = true;
            btn.textContent = '📚 学习中...';
            await autoStudyAll();
            btn.disabled = false;
            btn.textContent = '📚 自动学习';
        };
        
        document.getElementById('btn-auto-exam').onclick = async () => {
            const btn = document.getElementById('btn-auto-exam');
            btn.disabled = true;
            btn.textContent = '📝 考试中...';
            await autoExam();
            btn.disabled = false;
            btn.textContent = '📝 自动考试';
        };
        
        document.getElementById('btn-download-answers').onclick = async () => {
            log('正在下载题库...');
            try {
                await downloadAnswers();
                log('题库下载成功!');
            } catch (e) {
                log('题库下载失败: ' + e.message);
            }
        };
        
        document.getElementById('btn-upload-answers').onclick = () => {
            triggerUpload();
        };
        
        document.getElementById('btn-export-answers').onclick = () => {
            exportAnswers();
        };
        
        document.getElementById('btn-close-panel').onclick = () => {
            panel.style.display = 'none';
        };

        // 更新登录状态
        updateLoginStatus();
        
        // 定时更新登录状态
        setInterval(updateLoginStatus, 5000);
    }

    // 注册油猴菜单命令
    function registerMenuCommands() {
        GM_registerMenuCommand('📚 自动学习', autoStudyAll);
        GM_registerMenuCommand('📝 自动考试', autoExam);
        GM_registerMenuCommand('⬇️ 下载题库', async () => {
            try {
                await downloadAnswers();
                alert('题库下载成功!');
            } catch (e) {
                alert('题库下载失败: ' + e.message);
            }
        });
        GM_registerMenuCommand('⬆️ 上传题库', triggerUpload);
        GM_registerMenuCommand('📤 导出题库', exportAnswers);
    }

    // ==================== 课程页面自动完成 ====================
    
    // 在课程页面自动完成
    function handleCoursePage() {
        if (location.href.startsWith('https://mcwk.mycourse.cn')) {
            console.log('[自动完成课程] 当前页面符合要求,等待 13 秒...');
            setTimeout(() => {
                if (typeof finishWxCourse === 'function') {
                    console.log('[自动完成课程] 执行 finishWxCourse()');
                    finishWxCourse();
                } else {
                    console.warn('[自动完成课程] 未找到 finishWxCourse 函数');
                }
            }, 13000);
        }
    }

    // 点击返回按钮
    function clickReturnButton() {
        let attempts = 0;
        const max = 10;
        const timer = setInterval(() => {
            attempts++;
            const btns = document.querySelectorAll('.comment-footer-button');
            for (const btn of btns) {
                if (btn.textContent.includes('返回列表')) {
                    console.log('[自动课程] 点击"返回列表"按钮');
                    btn.click();
                    clearInterval(timer);
                    return;
                }
            }
            if (attempts >= max) {
                console.warn('[自动课程] 未找到返回按钮');
                clearInterval(timer);
            }
        }, 500);
    }

    // ==================== URL 变化监听 ====================
    
    function onUrlChange(callback) {
        let oldHref = location.href;
        const observer = new MutationObserver(() => {
            if (location.href !== oldHref) {
                oldHref = location.href;
                callback(location.href);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ==================== 主逻辑 ====================
    
    function runLogic(url) {
        // 课程页面自动完成
        handleCoursePage();

        // 评论页返回
        if (url.startsWith('https://weiban.mycourse.cn/#/wk/comment')) {
            console.log('[自动课程] 进入评论页,尝试点击"返回列表"按钮...');
            clickReturnButton();
        }
    }

    // 初始化
    function init() {
        // 注册油猴菜单
        registerMenuCommands();

        // 只在 weiban.mycourse.cn 主站显示控制面板
        if (location.hostname === 'weiban.mycourse.cn') {
            // 等待页面加载完成
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', createControlPanel);
            } else {
                createControlPanel();
            }
        }

        // 初次执行
        runLogic(location.href);

        // 监听路由变化
        onUrlChange((newUrl) => {
            console.log('[自动课程] 路由变化:', newUrl);
            runLogic(newUrl);
        });
    }

    init();
})();

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions