Greasy Fork is available in English.

安全微伴 2025-08 可刷课程 (by 浩劫者12345, modified by Houtar)

安全微伴, 非自动点击版, 直接调API, 速度快. 可刷课程和考试. 受服务端限制, 每 13 秒只能学习一个课程, 超出这个频率即使返回成功也不会更新学习记录. 多线程同时学习多个课程也没用

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         安全微伴 2025-08 可刷课程 (by 浩劫者12345, modified by Houtar)
// @namespace    https://weiban.houtar.me/
// @version      25.8.7.1
// @description  安全微伴, 非自动点击版, 直接调API, 速度快. 可刷课程和考试. 受服务端限制, 每 13 秒只能学习一个课程, 超出这个频率即使返回成功也不会更新学习记录. 多线程同时学习多个课程也没用
// @author       浩劫者12345, modified by Houtar
// @match        https://weiban.mycourse.cn/
// @grant        none
// @license      MIT
// ==/UserScript==
 
// @ts-check
 
(async function () {
    'use strict';
 
    const vConsole = (() => {
        const el = document.createElement('div')
        el.classList.add('weiban-console')
        el.innerHTML = /*html*/`
            <div class="console-header">
                <span>安全微伴助手</span>
                <button class="btn-learn">开始刷课</button>
                <span style="font-size: 13px">
                    <span>间隔</span>
                    <input class="learn-interval" value="13500" style="width: 60px">
                    <span>ms</span>
                <button class="btn-exam">开始刷考试</button>
                </span>
            </div>
            <div class="console-text-wrapper"></div>
        `;
        const textEl = /** @type {HTMLDivElement} */(el.querySelector('.console-text-wrapper'))
        const learnIntervalInput = /** @type {HTMLInputElement} */(el.querySelector('.learn-interval'))
 
        const style = document.createElement('style')
        style.innerHTML = /*css*/`
            .weiban-console {
                position: fixed;
                right: 0;
                bottom: 0;
                background-color: rgba(255, 255, 255, 0.8);
                font-size: 16px;
                z-index: 9999;
            }
 
            .console-header {
                padding: 4px;
                background-color: rgba(0, 0, 0, 0.1);
            }
 
            .console-header button {
                font-size: 13px;
                padding: 0 2px;
            }
 
            .weiban-console .console-text-wrapper {
                width: 400px;
                height: 400px;
                font-size: 14px;
                overflow: auto;
                white-space: pre;
            }
 
            .weiban-console ::-webkit-scrollbar {
                display: block;
            }
 
           .weiban-console ::-webkit-scrollbar-thumb {
                background-color: rgba(0, 0, 0, 0.15);
            }
        `;
 
        document.body.appendChild(el)
        document.head.appendChild(style)
 
        /**
         * @param {HTMLButtonElement} btnEl 
         * @param {TaskFunction} taskFn
         * @param {string} startText 
         * @param {string} stoppText 
         */
        function initToggleButton(btnEl, taskFn, startText, stoppText) {
            /** @type {ReturnType<createTask> | undefined} */
            let task;
            btnEl.onclick = () => {
                if (task == undefined || task.stopped) {
                    task = createTask(taskFn)
                    task.onFinish = () => {
                        btnEl.textContent = startText
                    }
                    btnEl.textContent = stoppText
                } else {
                    task.stop()
                }
            }
        }
 
        initToggleButton(
            /** @type {HTMLButtonElement} */(el.querySelector('.btn-learn')),
            learnAllCourses,
            '开始刷课',
            '停止刷课'
        )
        initToggleButton(
            /** @type {HTMLButtonElement} */(el.querySelector('.btn-exam')),
            takeAllExams,
            '开始刷考试',
            '停止刷考试'
        )
 
        const self = {
            /** @param {string} obj */
            log(obj) {
                const str = typeof obj == 'string' ? obj : obj
                const toBottom = textEl.scrollHeight - textEl.scrollTop - textEl.clientHeight
 
                const line = document.createElement('div')
                line.textContent = str
                textEl.appendChild(line)
 
                if (toBottom < 50) {
                    textEl.scrollTop = textEl.scrollHeight
                }
            },
            getLearnInterval() {
                return parseInt(learnIntervalInput.value)
            },
        }
 
        return self
    })()
 
    /**
     * @typedef {object} User
     * @property {string} token
     * @property {string} userId
     * @property {string} userName
     * @property {string} realName
     * @property {string} userNameLabel
     * @property {string} uniqueValue
     * @property {string} isBind
     * @property {string} tenantCode
     * @property {string} batchCode
     * @property {number} gender
     * @property {number} switchGoods
     * @property {number} switchDanger
     * @property {number} switchNetCase
     * @property {string} preBanner
     * @property {string} normalBanner
     * @property {string} specialBanner
     * @property {string} militaryBanner
     * @property {number} isLoginFromWechat
     * @property {string} tenantName
     * @property {number} tenantType
     * @property {number} loginSide
     * @property {number} popForcedCompleted
     * @property {number} showGender
     * @property {number} showOrg
     * @property {string} orgLabel
     * @property {string} nickName
     * @property {string} imageUrl
     * @property {number} defensePower
     * @property {number} knowledgePower
     * @property {number} safetyIndex
     */
 
 
    /** @type {User} */
    let User
 
    function loadUser() {
        const userStorage = localStorage.getItem('user')
        User = /** @type {User} */(userStorage ? JSON.parse(userStorage) : null)
        if (!User) {
            vConsole.log('用户未登录! 请登录后刷新页面以生效')
        }
    }
 
    loadUser()
 
    async function listAllProjects() {
        const projects = await getProjectList();
        if (projects) {
            vConsole.log('获取到用户学习项目:')
            if (projects.length == 0) {
                vConsole.log('没有学习项目')
            }
            for (const project of projects) {
                vConsole.log(project.projectName)
            }
        } else {
            vConsole.log('获取用户学习项目失败')
        }
        return projects
    }
 
    listAllProjects()
 
    /**
     * @typedef {object} TaskOptions
     * @property {boolean} stopFlag 
     */
 
    /** @typedef {(taskOptions: TaskOptions) => Promise} TaskFunction */
 
    /** @param {TaskFunction} taskFunction  */
    function createTask(taskFunction) {
        /** @type {TaskOptions} */
        let taskOptions = {
            stopFlag: false,
        }
        let stopped = false
 
        const task = {
            stop() {
                taskOptions.stopFlag = true
            },
            get stopped() {
                return stopped
            },
            onFinish: () => undefined,
        }
 
        taskFunction(taskOptions).finally(() => {
            stopped = true
            task.onFinish()
        })
 
        return task
    }
 
    /** @type {TaskFunction}  */
    async function learnAllCourses(taskOptions) {
        const projects = await listAllProjects()
 
        if (!projects || !projects.length) return
 
        for (const project of projects) {
            if (taskOptions.stopFlag) return
            vConsole.log(`获取项目 "${project.projectName}" 的课程分类:`);
 
            const categories = await getCourseCategories(project.userProjectId);
            if (!categories) {
                vConsole.log(`获取失败`)
                continue
            }
            if (categories.length == 0) {
                vConsole.log(`没有分类`)
                continue
            }
 
            for (const category of categories) {
                if (taskOptions.stopFlag) return
                vConsole.log(`- ${category.categoryName} (已完成: ${category.finishedNum} / ${category.totalNum})`);
 
                const courses = await getCourses(project.userProjectId, category.categoryCode);
                if (!courses) {
                    vConsole.log(`   获取课程失败`);
                    continue
                }
                if (courses.length == 0) continue
 
                for (const course of courses) {
                    if (taskOptions.stopFlag) return
                    vConsole.log(`   - ${course.resourceName}${course.finished == 1 ? ' (已完成)' : ''}`);
                    if (course.finished == 1) continue
 
                    vConsole.log(`       开始学习课程`);
                    if (!await startStudy(project.userProjectId, course.resourceId)) {
                        vConsole.log(`       失败`);
                        continue
                    }
 
                    vConsole.log(`       等待 ${vConsole.getLearnInterval()} ms, 服务器限制学习频率, 请耐心等待`);
                    await sleep(vConsole.getLearnInterval())
                    if (taskOptions.stopFlag) return
 
                    vConsole.log(`       完成学习`);
                    vConsole.log((await finishStudy(course.userCourseId))
                        ? `       成功`
                        : `       失败`
                    )
                }
            }
        }
    }
 
    /** @type {TaskFunction}  */
    async function takeAllExams(taskOptions) {
        const projects = await listAllProjects()
 
        if (!projects || !projects.length) return
 
        /** @param {Exam} exam  */
        function printExam(exam) {
            vConsole.log(`- ${exam.examPlanName}  最高成绩: ${exam.examScore} 分  已答 ${exam.examFinishNum} / ${exam.answerNum} 次${(exam.isRetake == 1 && exam.examType == 1) ? '  (补考)' : ''}`)
        }
 
        /**
         * @param {Exam} exam
         * @param {(question: ExamQuestion, answers: string[]) => void} questionHandler
         * @returns {Promise<boolean>}
         */
        async function startExamWithCallback(exam, questionHandler) {
            vConsole.log(`    开始考试`)
            const examQuestions = await startExam(exam.id)
            if (!examQuestions) {
                vConsole.log(`    失败\n    尝试调用页面验证码进入考试\n    请完成验证码后重试`)
                await gotoProject()
                gotoExam(exam)
                return false
            }
 
            for (const question of examQuestions) {
                if (taskOptions.stopFlag) return false
                vConsole.log(`    ${question.title}`)
 
                /** @type {string[]} */
                const answers = []
 
                questionHandler(question, answers)
 
                vConsole.log((await submitAnswer(exam.examPlanId, exam.id, question.id, answers.join(',')))
                    ? `    成功`
                    : `    失败`
                )
            }
 
            for (let i = 5; i > 0; i--) {
                vConsole.log(`    等待 ${i} 秒后交卷, 按 "停止刷考试" 取消交卷`)
                await sleep(1000)
                if (taskOptions.stopFlag) {
                    vConsole.log(`    取消交卷`)
                    return false
                }
            }
 
            const submitResult = await submitPaper(exam.id)
            vConsole.log(`    ${submitResult ? `交卷成功, 分数: ${submitResult.score}${submitResult.score < 100 ? '\n    没有满分的可以多刷几次, 因为考的次数越多, 能查到的题就越多' : ''}` : '交卷失败!'}`)
 
            return true
        }
 
        for (const project of projects) {
            if (taskOptions.stopFlag) return
            vConsole.log(`获取项目 "${project.projectName}" 的考试:`);
 
            const exams = await getExams(project.userProjectId)
            if (!exams) {
                vConsole.log(`获取失败`)
                continue
            }
            if (exams.length == 0) {
                vConsole.log(`没有考试`)
                continue
            }
 
            for (const exam of exams) {
                printExam(exam)
            }
 
            vConsole.log(`查找做过的考试:`)
            const doneExams = exams.filter(x => x.examFinishNum)
 
            if (doneExams.length == 0) {
                vConsole.log(`找不到已完成考试\n我们需要先故意做错至少一个考试, 获取到答案再做补考`)
 
                const todoExam = exams[0]
                printExam(todoExam)
 
                if (!await startExamWithCallback(todoExam, (question, answers) => {
                    answers.push(question.optionList[0].id)
                })) continue
 
                vConsole.log(`    初次考试已完成\n    再次运行 "刷考试" 即可根据初次考试的答案刷补考`)
                continue
            }
 
            doneExams.forEach(x => printExam(x))
 
            /** @type {ExamQuestionAnswer[]} */
            const examAnswers = []
 
            for (const doneExam of doneExams) {
                if (taskOptions.stopFlag) return
                vConsole.log(`获取答题记录`)
 
                const examHistory = await getExamHistory(doneExam.examPlanId, doneExam.examType)
                if (!examHistory) {
                    vConsole.log(`获取失败`)
                    continue
                }
 
                vConsole.log(`正在获取 ${examHistory.length} 个答题分析和答案`)
                for (const history of examHistory) {
                    if (taskOptions.stopFlag) return
 
                    const answers = await getExamAnswer(history.id)
                    if (!answers) {
                        vConsole.log(`获取失败`)
                        continue
                    }
 
                    for (const answer of answers) {
                        if (examAnswers.find(x => x.title == answer.title)) continue
                        examAnswers.push(answer)
                    }
 
                    vConsole.log(`成功`)
                }
            }
 
            vConsole.log(`成功获取 ${examAnswers.length} 条答案`)
 
            vConsole.log(`查找未完成考试:`)
            const todoExam = exams.find(x => x.examOddNum && x.examScore < 100)
            if (!todoExam) {
                vConsole.log(`所有考试都已完成!`)
                continue
            }
            printExam(todoExam)
 
            await startExamWithCallback(todoExam, (question, answers) => {
                const answer = examAnswers.find(x => x.title == question.title)
                if (!answer) {
                    vConsole.log(`    找不到本题答案`)
                    return
                }
                const correctAnswers = answer.optionList.filter(x => x.isCorrect == 1).map(x => x.content)
 
                for (const option of question.optionList) {
                    if (correctAnswers.includes(option.content)) {
                        vConsole.log(`    ${option.content}`)
                        answers.push(option.id)
                    }
                }
            })
        }
    }
 
    /** @param {number} ms */
    async function sleep(ms) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(null)
            }, ms);
        })
    }
 
 
    // page navigation
    //
 
    /** @param {string} [projectId] */
    async function gotoProject(projectId) {
        const targetHash = `#/course?projectId=${projectId}&projectType=special`
        if (projectId && location.hash == targetHash) return
        if (!projectId && location.hash.startsWith('#/course?')) return
        location.hash = targetHash
        await sleep(500)
    }
 
    /**
     * @param {Exam} exam
     * @returns {boolean}
     */
    function gotoExam(exam) {
        try {
            // @ts-expect-error
            document.querySelector('#app>.page').__vue__.navToExamDetail(exam)
            return true
        } catch (error) {
            return false
        }
    }
 
 
    // API
    //
 
    /**
     * @template T
     * @typedef {object} CourseApiResponse
     * @property {string} code
     * @property {T?} data
     * @property {string} detailCode
     * @property {string?} msg
     */
 
    /**
     * @template T
     * @param {string} url
     * @param {Record<string, string>} params
     * @returns {Promise<CourseApiResponse<T> | undefined>}
     */
    async function CourseApiRequest(url, params) {
        const requestBody = new URLSearchParams()
        for (const key in params) {
            requestBody.append(key, params[key])
        }
 
        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
                    'X-Token': User.token,
                },
                body: requestBody.toString(), // Convert URLSearchParams to a string
                credentials: 'same-origin' // Ensure cookies are sent with same-origin requests
            });
 
            if (!response.ok) {
                const errorText = await response.text();
                vConsole.log(`请求失败! Status: ${response.status}, Message: ${errorText}`);
                return
            }
 
            /** @type {CourseApiResponse<T>} */
            const data = await response.json();
 
            if (data.code != '0') {
                vConsole.log(`请求返回错误码: Code: ${data.code}, DetailCode: ${data.detailCode}, Msg: ${data.msg}`);
            }
            return data
 
        } catch (error) {
            vConsole.log(`请求出现未知错误:\n${error}`);
        }
    }
 
    /**
     * @typedef {object} Project
     * @property {string} projectId
     * @property {string} projectName
     * @property {string} projectImageUrl
     * @property {string} endTime
     * @property {number} finished
     * @property {number} progressPet
     * @property {number} exceedPet
     * @property {string} assessment
     * @property {string} userProjectId
     * @property {number} projectMode
     * @property {number} projectCategory
     * @property {number} projectAttribute
     * @property {number} studyState
     * @property {string} studyStateLabel
     * @property {number} certificateAcquired
     * @property {object} completion
     * @property {number} completion.marked
     * @property {number} completion.finished
     * @property {number} completion.grey
     * @property {number} completion.active
     * @property {string} completion.message
     */
 
    /**
     * 获取用户学习项目
     * @returns {Promise<Project[] | undefined>}
     */
    async function getProjectList() {
        const url = `https://weiban.mycourse.cn/pharos/index/listMyProject.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            ended: '2',
        };
        const response = /** @type {CourseApiResponse<Project[]> | undefined} */(await CourseApiRequest(url, params))
        return Array.isArray(response?.data) ? response.data : undefined
    }
 
    /** @typedef {object} CourseCategory
     * @property {string} categoryCode
     * @property {string} categoryName
     * @property {string} categoryRemark
     * @property {number} totalNum
     * @property {number} finishedNum
     * @property {string} categoryImageUrl
     */
 
    /**
     * 获取指定学习项目下的课程分类列表
     * @param {string} userProjectId
     * @returns {Promise<CourseCategory[] | undefined>}
     */
    async function getCourseCategories(userProjectId) {
        const url = `https://weiban.mycourse.cn/pharos/usercourse/listCategory.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            userProjectId,
            chooseType: '3',
        };
        const response = /** @type {CourseApiResponse<CourseCategory[]> | undefined} */(await CourseApiRequest(url, params))
        return Array.isArray(response?.data) ? response.data : undefined
    }
 
    /** @typedef {object} Course
     * @property {string} userCourseId
     * @property {string} resourceId
     * @property {string} resourceName
     * @property {number} finished 1 = 已完成, 2 = 未完成
     * @property {number} isPraise
     * @property {number} isShare
     * @property {number} praiseNum
     * @property {number} shareNum
     * @property {number} shared
     * @property {number} source
     * @property {string} imageUrl
     * @property {string} categoryName
     */
 
    /**
     * 获取指定学习项目和分类下的课程列表
     * @param {string} userProjectId
     * @param {string} categoryCode
     * @returns {Promise<Course[] | undefined>}
     */
    async function getCourses(userProjectId, categoryCode) {
        const url = `https://weiban.mycourse.cn/pharos/usercourse/listCourse.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            userProjectId,
            categoryCode,
            chooseType: '3',
        };
        const response = /** @type {CourseApiResponse<Course[]> | undefined} */(await CourseApiRequest(url, params))
        return Array.isArray(response?.data) ? response.data : undefined
    }
 
    /**
     * 开始学习指定课程, 上传进度前需要先开始学习, 不然状态不更新
     * @param {string} userProjectId
     * @param {string} resourceId
     * @returns {Promise<boolean>}
     */
    async function startStudy(userProjectId, resourceId) {
        const url = `https://weiban.mycourse.cn/pharos/usercourse/study.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            userProjectId,
            courseId: resourceId,
        };
        const response = /** @type {CourseApiResponse<undefined> | undefined} */(await CourseApiRequest(url, params))
        return response?.code == '0'
    }
 
    /**
     * 用 `userCourseId` 完成学习
     * @param {string} userCourseId
     * @returns {Promise<boolean>}
     */
    async function finishStudy(userCourseId) {
        const url = new URL(`https://weiban.mycourse.cn/pharos/usercourse/v2/${userCourseId}.do`);
        url.searchParams.append('userCourseId', userCourseId);
        url.searchParams.append('tenantCode', User.tenantCode);
        url.searchParams.append('callback', 'jQuery341011962447562795464_' + Date.now().toString());
        url.searchParams.append('_', Date.now().toString());
 
        try {
            const response = await fetch(url.toString(), {
                method: 'GET',
                credentials: 'same-origin'
            });
 
            if (!response.ok) {
                const errorText = await response.text();
                vConsole.log(`完成学习失败! Status: ${response.status}, Message: ${errorText}`);
            }
 
            const text = await response.text();
            /** @type {CourseApiResponse<undefined>} */
            const data = text.includes('jQuery') ? JSON.parse(text.split('(')[1].split(')')[0]) : JSON.parse(text)
 
            if (data.code == '0') {
                return true
            } else {
                vConsole.log(`完成学习接口返回错误代码: Code: ${data.code}, DetailCode: ${data.detailCode}, Msg: ${data.msg}`);
            }
 
        } catch (error) {
            vConsole.log(`完成学习时出现未知错误:\n${error}`);
        }
 
        return false
    }
 
    /** @typedef {object} Exam
     * @property {string} id 通常作为 `userExamPlanId` 调用其他考试接口
     * @property {string} examPlanId
     * @property {string} examPlanName
     * @property {number} answerNum 总可考次数
     * @property {number} answerTime
     * @property {number} passScore
     * @property {number} isRetake 补考
     * @property {number} examType 1 = 补考, 2 = 普通考试
     * @property {number} isAssessment
     * @property {string} startTime
     * @property {string} endTime
     * @property {number} examFinishNum 完成次数
     * @property {number} examOddNum 剩余次数
     * @property {number} examScore
     * @property {number} examTimeState
     * @property {number} displayState
     * @property {string} prompt
     */
 
    /**
     * 获取考试列表
     * @param {string} userProjectId
     * @returns {Promise<Exam[] | undefined>}
     */
    async function getExams(userProjectId) {
        const url = `https://weiban.mycourse.cn/pharos/exam/listPlan.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            userProjectId,
        };
        const response = /** @type {CourseApiResponse<Exam[]> | undefined} */(await CourseApiRequest(url, params))
        return Array.isArray(response?.data) ? response.data : undefined
    }
 
    /** @typedef {object} ExamHistory
     * @property {string} id
     * @property {number} score
     * @property {number} useTime
     * @property {string} submitTime
     * @property {number} passScore
     * @property {number} isRetake
     */
 
    /**
     * 获取考试历史记录
     * @param {string} examPlanId
     * @param {number} examType
     * @returns {Promise<ExamHistory[] | undefined>}
     */
    async function getExamHistory(examPlanId, examType) {
        const url = `https://weiban.mycourse.cn/pharos/exam/listHistory.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            examPlanId,
            examType: examType.toString(),
        };
        const response = /** @type {CourseApiResponse<ExamHistory[]> | undefined} */(await CourseApiRequest(url, params))
        return Array.isArray(response?.data) ? response.data : undefined
    }
 
    /** @typedef {object} ExamQuestionAnswer
     * @property {string} title
     * @property {number} type
     * @property {string} typeLabel
     * @property {number} score
     * @property {number} sequence
     * @property {string} analysis
     * @property {number} isRight
     * @property {object[]} optionList
     * @property {string} optionList.content
     * @property {number} optionList.sequence
     * @property {number} optionList.selected
     * @property {number} optionList.isCorrect
     * @property {unknown[]} optionList.attachmentList
     * @property {unknown[]} attachmentList
     */
 
    /** @typedef {object} ExamReview
     * @property {string} submitTime
     * @property {number} score
     * @property {number} useTime
     * @property {ExamQuestionAnswer[]} questions
     */
 
    /**
     * 获取考试答题记录和正确答案
     * @param {string} userExamId
     * @returns {Promise<ExamQuestionAnswer[] | undefined>}
     */
    async function getExamAnswer(userExamId) {
        const url = `https://weiban.mycourse.cn/pharos/exam/reviewPaper.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            userExamId,
            isRetake: '2',
        };
        const response = /** @type {CourseApiResponse<ExamReview> | undefined} */(await CourseApiRequest(url, params))
        return Array.isArray(response?.data?.questions) ? response.data.questions : undefined
    }
 
    /** @typedef {object} ExamQuestion
     * @property {string} id
     * @property {string} title
     * @property {number} type
     * @property {string} typeLabel
     * @property {number} score
     * @property {number} sequence
     * @property {number} isRight
     * @property {object[]} optionList
     * @property {string} optionList.id
     * @property {string} optionList.questionId
     * @property {string} optionList.content
     * @property {number} optionList.sequence
     * @property {number} optionList.selected
     * @property {unknown[]} optionList.attachmentList
     * @property {unknown[]} attachmentList
     */
 
    /** @typedef {object} ExamPaper
     * @property {number} answerTime
     * @property {ExamQuestion} questionList
     */
 
    /**
     * 开始考试
     * @param {string} userExamPlanId
     * @returns {Promise<ExamQuestion[] | undefined>}
     */
    async function startExam(userExamPlanId) {
        const url = `https://weiban.mycourse.cn/pharos/exam/startPaper.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            userExamPlanId,
        };
        const response = /** @type {CourseApiResponse<ExamPaper> | undefined} */(await CourseApiRequest(url, params))
        return Array.isArray(response?.data?.questionList) ? response.data.questionList : undefined
    }
 
    /**
     * 提交答案
     * @param {string} examPlanId
     * @param {string} userExamPlanId
     * @param {string} questionId
     * @param {string} answerIds 逗号分隔
     * @returns {Promise<boolean>}
     */
    async function submitAnswer(examPlanId, userExamPlanId, questionId, answerIds) {
        const url = `https://weiban.mycourse.cn/pharos/exam/recordQuestion.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            examPlanId,
            userExamPlanId,
            questionId,
            answerIds,
            useTime: '10',
        };
        const response = /** @type {CourseApiResponse<ExamPaper> | undefined} */(await CourseApiRequest(url, params))
        return response?.code == '0'
    }
 
    /** @typedef {object} SubmitPaperResult
     * @property {number} score
     * @property {object} redpacketInfo
     * @property {string} redpacketInfo.redpacketName
     * @property {string} redpacketInfo.redpacketComment
     * @property {number} redpacketInfo.redpacketMoney
     * @property {number} redpacketInfo.isSendRedpacket
     * @property {object} ebookInfo
     * @property {number} ebookInfo.displayBook
     */
 
    /**
     * 交卷
     * @param {string} userExamPlanId
     * @returns {Promise<SubmitPaperResult | undefined>}
     */
    async function submitPaper(userExamPlanId) {
        const url = `https://weiban.mycourse.cn/pharos/exam/submitPaper.do`;
        const params = {
            tenantCode: User.tenantCode,
            userId: User.userId,
            userExamPlanId,
        };
        const response = /** @type {CourseApiResponse<SubmitPaperResult> | undefined} */(await CourseApiRequest(url, params))
        return (response?.code == '0' && response.data) ? response.data : undefined
    }
 
})();