Greasy Fork

来自缓存

Greasy Fork is available in English.

华医网自动化考试助手 (V2.3.3 - Key优化版)

【Key逻辑优化】构造Key时,增加去除所有括号和空格的步骤,大幅提升匹配的稳定性和容错性。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         华医网自动化考试助手 (V2.3.3 - Key优化版)
// @namespace    http://tampermonkey.net/
// @version      2.3.3
// @description  【Key逻辑优化】构造Key时,增加去除所有括号和空格的步骤,大幅提升匹配的稳定性和容错性。
// @author       Gemini (Key Optimized)
// @match        *://*.91huayi.com/pages/course.aspx*
// @match        *://*.91huayi.com/pages/exam.aspx*
// @match        *://*.91huayi.com/pages/exam_result.aspx*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置项 ---
    const BASE_ANSWER_DELAY_MS = 500;
    const BASE_SUBMIT_DELAY_MS = 2000;
    const BASE_RETRY_DELAY_MS = 3000;
    const SCRIPT_STATE_KEY = 'exam_script_state_v11'; // 版本号更新
    const COURSE_LIST_URL_FRAGMENT = '/pages/course.aspx';

    // --- 辅助函数 ---
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
    const getRandomDelay = (base, range = 1500) => base + Math.random() * range;

    // --- 数据存储模块 ---
    const db = {
        load: (key, defaultValue) => {
            const value = GM_getValue(key);
            return value === undefined ? defaultValue : JSON.parse(value);
        },
        save: (key, data) => GM_setValue(key, JSON.stringify(data)),
        clear: (key) => GM_deleteValue(key)
    };

    // --- 数据库定义 (版本号更新) ---
    const correctAnswersDB = { key: 'correct_answers_db_v11', get: () => db.load(correctAnswersDB.key, {}), set: (data) => db.save(correctAnswersDB.key, data) };
    const wrongAttemptsDB = { key: 'wrong_attempts_db_v11', get: () => db.load(wrongAttemptsDB.key, {}), set: (data) => db.save(wrongAttemptsDB.key, data), clear: () => db.clear(wrongAttemptsDB.key) };
    const examQueueDB = { key: 'exam_queue_db_v11', get: () => db.load(examQueueDB.key, []), set: (data) => db.save(examQueueDB.key, data), clear: () => db.clear(examQueueDB.key) };
    const returnUrlDB = { key: 'return_url_db_v11', get: () => db.load(returnUrlDB.key, null), set: (data) => db.save(returnUrlDB.key, data), clear: () => db.clear(returnUrlDB.key) };
    const questionOptionMapDB = { key: 'question_option_map_v11', get: () => db.load(questionOptionMapDB.key, {}), set: (data) => db.save(questionOptionMapDB.key, data), clear: () => db.clear(questionOptionMapDB.key) };
    const debugKeysDB = { key: 'debug_keys_db_v11', get: () => db.load(debugKeysDB.key, []), set: (data) => db.save(debugKeysDB.key, data), clear: () => db.clear(debugKeysDB.key) };


    // --- 核心逻辑 ---

    // --- V2.3.3 核心修改:优化Key的构造逻辑 ---
    function normalizeQuestion(text) {
        if (!text) return '';
        return text
            .trim()                          // 1. 去除首尾空格
            .replace(/^\d+、\s*/, '')      // 2. 去除题号
            .replace(/[()()]/g, '')        // 3. 【新增】移除所有括号
            .replace(/\s+| /g, '')         // 4. 【新增】移除所有空白符(包括全角空格)
            .trim();                         // 5. 最后再trim一次确保万无一失
    }

    function normalizeOption(text) {
        if (!text) return '';
        // 选项的标准化逻辑也统一,确保一致性
        return text
            .trim()
            .replace(/^[A-Z]、\s*/, '')
            .replace(/[()()]/g, '')
            .replace(/\s+| /g, '')
            .trim();
    }

    function getOptionInfo(optionEl) {
        if (!optionEl) return null;
        const label = optionEl.querySelector('label');
        if (!label) return null;
        const rawText = label.innerText;
        const match = rawText.trim().match(/^([A-Z])、/);
        return {
            letter: match ? match[1] : null,
            content: normalizeOption(rawText), // 使用标准化的选项内容
            element: optionEl.querySelector('input.qo_name')
        };
    }

    function handleCourseListPage() {
        console.log("脚本:进入课程列表页面。");
        const allCourses = document.querySelectorAll('.course');
        if (allCourses.length === 0) return;

        const pendingExams = [];
        allCourses.forEach(courseEl => {
            const statusSpan = courseEl.querySelector('h3 > span');
            const link = courseEl.querySelector('h3 > a.f14blue');
            if (link && statusSpan && !statusSpan.innerText.includes('已完成')) {
                const courseName = link.innerText.trim();
                const href = link.getAttribute('href');
                const cwidMatch = href.match(/cwid=([a-f0-9-]+)/);
                if (cwidMatch && cwidMatch[1]) {
                    pendingExams.push({ cwid: cwidMatch[1], name: courseName });
                }
            }
        });

        if (pendingExams.length > 0) {
            console.log(`脚本:发现 ${pendingExams.length} 个待考课程。已创建考试队列。`);
            examQueueDB.set(pendingExams);
            returnUrlDB.set(window.location.href);
            updatePanel();
            processNextInQueue();
        } else {
            console.log("脚本:所有课程均已完成,无需操作。");
            alert("太棒了!所有课程都已完成!");
            setScriptState(false);
            updatePanel();
        }
    }

    async function handleExamPage() {
        console.log("脚本:进入考试页面。开始根据内容智能作答...");
        const correctAnswers = correctAnswersDB.get();
        const wrongAttempts = wrongAttemptsDB.get();
        const questions = document.querySelectorAll('.tablestyle');
        const questionOptionMap = {};
        const examPageGeneratedKeys = [];

        for (const questionEl of Array.from(questions)) {
            const questionKey = normalizeQuestion(questionEl.querySelector('.q_name').innerText);
            console.log(`[考试页Key生成]: "${questionKey}"`);
            examPageGeneratedKeys.push(questionKey);

            let isAnswered = false;

            const pageOptions = Array.from(questionEl.querySelectorAll('tbody tr')).map(getOptionInfo).filter(Boolean);

            const currentOptionMap = {};
            pageOptions.forEach(opt => {
                if(opt.letter && opt.content) currentOptionMap[opt.content] = opt.letter;
            });
            questionOptionMap[questionKey] = currentOptionMap;

            const knownCorrects = correctAnswers[questionKey] || [];
            if (knownCorrects.length > 0) {
                for (const knownGood of knownCorrects) {
                    for (const pageOpt of pageOptions) {
                        if (pageOpt.content === knownGood.content) {
                            pageOpt.element.click();
                            isAnswered = true; break;
                        }
                    }
                    if (isAnswered) break;
                }
            }

            if (!isAnswered) {
                const knownWrongs = wrongAttempts[questionKey] || [];
                const wrongContents = knownWrongs.map(item => item.content);
                for (const pageOpt of pageOptions) {
                    if (!wrongContents.includes(pageOpt.content)) {
                        pageOpt.element.click();
                        isAnswered = true; break;
                    }
                }
            }

            if (!isAnswered && pageOptions.length > 0) {
                 pageOptions[0].element.click();
                 isAnswered = true;
            }

            if (!isAnswered) {
                console.error("脚本错误:有题目未能作答,流程停止。"); setScriptState(false); updatePanel(); return;
            }
            await sleep(getRandomDelay(BASE_ANSWER_DELAY_MS, 1000));
        }

        console.log("脚本:保存本次考试的<问题-选项内容-字母>映射表...");
        questionOptionMapDB.set(questionOptionMap);
        debugKeysDB.set(examPageGeneratedKeys);

        const submitDelay = getRandomDelay(BASE_SUBMIT_DELAY_MS, 2000);
        console.log(`脚本:作答完毕,将在约 ${(submitDelay / 1000).toFixed(1)} 秒后提交...`);
        await sleep(submitDelay);
        document.getElementById('btn_submit')?.click();
    }

    async function handleResultPage() {
        console.log("脚本:进入结果页面,准备进行高保真学习。");
        const previousExamKeys = debugKeysDB.get();
        if (previousExamKeys.length > 0) {
            console.log("--- 考试页Key摘要 (用于对比) ---");
            console.table(previousExamKeys);
            console.log("---------------------------------");
            debugKeysDB.clear();
        }

        const questionMap = questionOptionMapDB.get();
        if(Object.keys(questionMap).length === 0){
            console.error("无法加载问题映射表,无法进行学习。");
            return;
        }

        const isPassed = !!document.querySelector('.tips_text')?.innerText.includes('考试通过');

        if (isPassed) {
            console.log("考试通过!准备处理下一个课程...");
            console.log("正在清除本次考试的答案库(适配不同考试)...");
            db.clear(correctAnswersDB.key);
            wrongAttemptsDB.clear();

            questionOptionMapDB.clear();
            const cwidMatch = window.location.href.match(/cwid=([a-f0-9-]+)/);
            if (cwidMatch) {
                const completedCwid = cwidMatch[1];
                let queue = examQueueDB.get();
                queue = queue.filter(exam => exam.cwid !== completedCwid);
                examQueueDB.set(queue);
            }
            await sleep(1000);
            processNextInQueue();
        } else {
            console.log("考试未通过。正在更新智能题库并准备重试...");
            const correctAnswers = correctAnswersDB.get();
            const wrongAttempts = wrongAttemptsDB.get();

            document.querySelectorAll('.state_cour_ul .state_cour_lis').forEach(item => {
                const questionKey = normalizeQuestion(item.querySelector('.state_lis_text:first-of-type').title);
                console.log(`[结果页Key生成]: "${questionKey}"`);

                const answerElement = item.querySelectorAll('.state_lis_text')[1];
                if (!answerElement) return;

                const userAnswerContent = normalizeOption(answerElement.innerText.replace(/【您的答案:|】/g, ''));
                const isCorrect = item.querySelector('.state_error').src.includes('bar_img.png');

                const optionsForThisQuestion = questionMap[questionKey];
                if (!optionsForThisQuestion) {
                    console.warn(`警告:在映射表中未找到问题 KEY: "${questionKey}"`);
                    return;
                }
                // 使用标准化的内容在映射表中反查字母
                const userAnswerLetter = optionsForThisQuestion[userAnswerContent];
                if(!userAnswerLetter) {
                    console.warn(`警告:在问题 "${questionKey}" 的映射中未找到答案内容 "${userAnswerContent}"`);
                    return;
                }

                const knowledgeBit = { letter: userAnswerLetter, content: userAnswerContent };

                if (isCorrect) {
                    if (!correctAnswers[questionKey]) correctAnswers[questionKey] = [];
                    if (!correctAnswers[questionKey].some(k => k.content === knowledgeBit.content)) {
                        correctAnswers[questionKey].push(knowledgeBit);
                    }
                    if (wrongAttempts[questionKey]) {
                        wrongAttempts[questionKey] = wrongAttempts[questionKey].filter(k => k.content !== knowledgeBit.content);
                        if (wrongAttempts[questionKey].length === 0) delete wrongAttempts[questionKey];
                    }
                } else {
                    if (!wrongAttempts[questionKey]) wrongAttempts[questionKey] = [];
                    if (!wrongAttempts[questionKey].some(k => k.content === knowledgeBit.content)) {
                        wrongAttempts[questionKey].push(knowledgeBit);
                    }
                }
            });
            correctAnswersDB.set(correctAnswers);
            wrongAttemptsDB.set(wrongAttempts);
            await sleep(getRandomDelay(BASE_RETRY_DELAY_MS, 2000));

            const retryButton = Array.from(document.querySelectorAll('input.state_foot_btn')).find(btn => btn.value === '重新考试');
            if(retryButton) retryButton.click(); else console.error("找不到“重新考试”按钮!");
        }
    }

    function processNextInQueue() {
        const queue = examQueueDB.get();
        if (queue.length > 0) {
            const nextExam = queue[0];
            console.log(`脚本:正在导航到下一个考试,课程名: ${nextExam.name}`);
            window.location.href = `/pages/exam.aspx?cwid=${nextExam.cwid}`;
        } else {
            console.log("队列已清空!所有考试均已完成!");
            const returnUrl = returnUrlDB.get();
            examQueueDB.clear(); returnUrlDB.clear(); wrongAttemptsDB.clear(); questionOptionMapDB.clear();
            setScriptState(false);
            alert("所有待考课程均已完成!脚本已停止。即将返回课程列表页。");
            setTimeout(() => { if (returnUrl) window.location.href = returnUrl; }, 2000);
        }
    }

    // --- UI 控制面板 ---
    function setupUI() {
        if (document.getElementById('exam-helper-panel')) return;
        const panel = document.createElement('div');
        panel.id = 'exam-helper-panel'; document.body.appendChild(panel);
        const dbViewer = document.createElement('div');
        dbViewer.id = 'db-viewer-panel';
        dbViewer.style.display = 'none';
        dbViewer.innerHTML = `<div id="db-viewer-header"><h3>数据库信息 (只读)</h3><span id="db-viewer-close-btn">&times;</span></div><div id="db-viewer-content"></div>`;
        document.body.appendChild(dbViewer);
        dbViewer.querySelector('#db-viewer-close-btn').addEventListener('click', () => dbViewer.style.display = 'none');
        GM_addStyle(`
            #exam-helper-panel { position: fixed; bottom: 20px; right: 20px; background-color: #f0f9ff; border: 2px solid #1e90ff; border-radius: 8px; padding: 15px; z-index: 9999; font-family: sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,0.15); width: 260px; font-size: 14px; }
            #exam-helper-panel h3 { margin: 0 0 12px 0; color: #1e90ff; text-align: center; font-size: 16px; }
            #exam-helper-panel button { width: 100%; padding: 8px; margin-bottom: 10px; border: none; border-radius: 5px; color: white; cursor: pointer; font-size: 14px; transition: background-color 0.2s; }
            #exam-helper-panel button:hover { opacity: 0.9; }
            #toggle-script-btn { background-color: #28a745; } #toggle-script-btn.running { background-color: #dc3545; }
            #clear-queue-btn { background-color: #007bff; } #view-db-btn { background-color: #6c757d; } #clear-db-btn { background-color: #ffc107; }
            #exam-helper-panel p { margin: 5px 0; line-height: 1.4; } #exam-helper-panel strong { color: #333; }
            #queue-list-container { background-color: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 5px 10px; margin-top: 10px; max-height: 150px; overflow-y: auto; font-size: 12px; }
            #db-viewer-panel { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80%; max-width: 800px; height: 80%; max-height: 600px; background-color: #fff; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.5); z-index: 10001; flex-direction: column; }
            #db-viewer-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; background-color: #f7f7f7; border-bottom: 1px solid #eee; }
            #db-viewer-close-btn { font-size: 24px; color: #888; cursor: pointer; font-weight: bold; }
            #db-viewer-content { padding: 15px; overflow: auto; flex-grow: 1; font-family: Consolas, Monaco, monospace; font-size: 13px; background-color: #fafafa; }
            #db-viewer-content h4 { margin-top: 0; color: #1e90ff; border-bottom: 1px solid #ddd; padding-bottom: 5px;}
            #db-viewer-content pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; }
        `);
        updatePanel();
    }

    function updatePanel() {
        const panel = document.getElementById('exam-helper-panel');
        if (!panel) return;
        const isRunning = getScriptState();
        const queue = examQueueDB.get();
        const currentExam = queue.length > 0 ? queue[0].name : "无";
        const remainingQueue = queue.slice(1);
        let queueHtml = '<p style="color:#888;text-align:center;font-style:italic;">队列为空</p>';
        if (remainingQueue.length > 0) queueHtml = '<ol style="margin:0;padding-left:20px;">' + remainingQueue.map(exam => `<li style="margin-bottom:5px;color:#555;">${exam.name}</li>`).join('') + '</ol>';
        panel.innerHTML = `
            <h3>自动考试控制 V2.3.3</h3>
            <button id="toggle-script-btn">${isRunning ? '暂停自动考试' : '开始自动考试'}</button>
            <button id="clear-queue-btn">清空队列</button>
            <button id="view-db-btn">查看数据库</button>
            <button id="clear-db-btn">清除全部数据(重要)</button>
            <p><strong>脚本状态:</strong> <span style="color: ${isRunning ? '#28a745' : '#dc3545'}; font-weight: bold;">${isRunning ? '运行中' : '已停止'}</span></p>
            <p><strong>当前操作:</strong> <span style="color: #007bff; font-weight: bold;">${currentExam}</span></p>
            <strong>待考列表:</strong>
            <div id="queue-list-container" style="background-color:#fff;border:1px solid #ddd;border-radius:4px;padding:5px 10px;margin-top:10px;max-height:150px;overflow-y:auto;font-size:12px;">${queueHtml}</div>
        `;
        if (isRunning) panel.querySelector('#toggle-script-btn').classList.add('running');
        panel.querySelector('#toggle-script-btn').addEventListener('click', toggleScript);
        panel.querySelector('#view-db-btn').addEventListener('click', showDbInfo);
        panel.querySelector('#clear-queue-btn').addEventListener('click', () => { if (confirm('确定要清空当前的考试队列吗?')) { examQueueDB.clear(); returnUrlDB.clear(); updatePanel(); } });
        panel.querySelector('#clear-db-btn').addEventListener('click', () => {
            if (confirm('【!!!重要!!!】\n确定要清除所有本地数据吗?\n\n新版脚本的数据库结构与旧版不兼容,必须清除旧数据才能正常工作!此操作不可逆!')) {
                db.clear(correctAnswersDB.key); db.clear(wrongAttemptsDB.key); db.clear(examQueueDB.key); db.clear(returnUrlDB.key); db.clear(questionOptionMapDB.key); db.clear(debugKeysDB.key);
                alert('所有数据已清除!脚本现在可以正常运行。'); updatePanel();
            }
        });
    }

    function showDbInfo() {
        const viewer = document.getElementById('db-viewer-panel');
        const contentEl = document.getElementById('db-viewer-content');
        if (!viewer || !contentEl) return;
        const correct = correctAnswersDB.get(); const wrong = wrongAttemptsDB.get();
        contentEl.innerHTML = `<h4>✔️ 正确答案库</h4><pre>${JSON.stringify(correct, null, 2)}</pre><hr style="margin:20px 0;"><h4>❌ 错题本</h4><pre>${JSON.stringify(wrong, null, 2)}</pre>`;
        viewer.style.display = 'flex';
    }

    function getScriptState() { return db.load(SCRIPT_STATE_KEY, false); }
    function setScriptState(isRunning) { db.save(SCRIPT_STATE_KEY, isRunning); }

    function toggleScript() {
        const currentState = getScriptState(); setScriptState(!currentState);
        if (!currentState) main(); else console.log("脚本已由用户暂停。");
        updatePanel();
    }

    function main() {
        setupUI();
        if (!getScriptState()) {
            console.log("脚本当前为停止状态,请在课程列表页点击【开始自动考试】按钮启动。");
            return;
        }
        const urlPath = window.location.pathname;
        if (urlPath.includes(COURSE_LIST_URL_FRAGMENT)) handleCourseListPage();
        else if (urlPath.includes('/exam.aspx')) handleExamPage();
        else if (urlPath.includes('/exam_result.aspx')) handleResultPage();
    }

    main();
})();