Skip to content

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

@yueseqaz

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();
})();

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