晚风知到答题小助手

智慧树章节测试自动答题

// ==UserScript==
// @name        晚风知到答题小助手
// @namespace    晚风
// @version      1.0
// @description  智慧树章节测试自动答题
// @author       晚风
// @match        *://*.zhihuishu.com/stuExamWeb*
// @connect      fengtingwanli.cn
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// @antifeature  ads
// @resource     css https://unpkg.com/element-ui/lib/theme-chalk/index.css
// ==/UserScript==
const logMessages = [];
let isCollapsed = false;
let windowState = {
    width: 500,
    height: 450,
    left: 20,
    top: 20,
    collapsed: false
};

function loadWindowState() {
    try {
        const savedState = GM_getValue('windowState', null);
        if (savedState) {
            windowState = JSON.parse(savedState);
            isCollapsed = windowState.collapsed;
        }
    } catch (e) {
        logMessage('加载窗口状态失败: ' + e.message, 'warning');
    }
}

function saveWindowState() {
    try {
        const floatingWindow = document.getElementById('floating-window');
        if (floatingWindow) {
            windowState.left = parseInt(floatingWindow.style.left);
            windowState.top = parseInt(floatingWindow.style.top);
            windowState.collapsed = isCollapsed;
            GM_setValue('windowState', JSON.stringify(windowState));
        }
    } catch (e) {
        logMessage('保存窗口状态失败: ' + e.message, 'warning');
    }
}

function logMessage(message, type = 'info') {
    const timestamp = new Date().toLocaleTimeString();
    logMessages.push({ timestamp, message, type });
    if (logMessages.length > 30) logMessages.shift();
    updateLogDisplay();
}
function showTip(message, type = 'info', duration = 2000) {
    const tipEl = document.createElement('div');
    tipEl.textContent = message;
    tipEl.style.position = 'relative';
    tipEl.style.display = 'inline-block';
    tipEl.style.marginTop = '8px';
    tipEl.style.padding = '6px 12px';
    tipEl.style.borderRadius = '4px';
    tipEl.style.color = 'white';
    tipEl.style.fontSize = '13px';
    tipEl.style.fontWeight = '500';
    tipEl.style.zIndex = '1000';
    tipEl.style.opacity = '0';
    tipEl.style.transition = 'opacity 0.3s';


    switch(type) {
        case 'success': tipEl.style.backgroundColor = '#67c23a'; break;
        case 'warning': tipEl.style.backgroundColor = '#e6a23c'; break;
        case 'error': tipEl.style.backgroundColor = '#f56c6c'; break;
        default: tipEl.style.backgroundColor = '#909399';
    }

    const apiContainer = document.getElementById('save-api-key-btn').parentElement;
    apiContainer.appendChild(tipEl);

    requestAnimationFrame(() => tipEl.style.opacity = '1');

    setTimeout(() => {
        tipEl.style.opacity = '0';
        setTimeout(() => tipEl.remove(), 300);
    }, duration);
}

function makeElementResizableDraggable(el) {
    loadWindowState();

    el.style.position = 'absolute';
    el.style.left = `${windowState.left}px`;
    el.style.top = `${windowState.top}px`;

    if (isCollapsed) {
        const floatingWindow = document.getElementById('floating-window');
        const collapseBtnIcon = document.querySelector('#collapse-btn i');

        if (!floatingWindow.dataset.originalHeight) {
            floatingWindow.dataset.originalHeight = `${floatingWindow.offsetHeight}px`;
        }

        floatingWindow.style.height = '40px';
        floatingWindow.style.overflow = 'hidden';

        if (collapseBtnIcon) collapseBtnIcon.className = 'el-icon-plus';
    }

    const header = el.querySelector('.el-dialog__header');
    header.style.cursor = 'move';

    header.onmousedown = function (event) {
        if (event.target.id === 'collapse-btn' || event.target.parentElement.id === 'collapse-btn') {
            return;
        }

        let shiftX = event.clientX - el.getBoundingClientRect().left;
        let shiftY = event.clientY - el.getBoundingClientRect().top;

        function moveAt(pageX, pageY) {
            el.style.left = pageX - shiftX + 'px';
            el.style.top = pageY - shiftY + 'px';
        }

        function onMouseMove(event) {
            moveAt(event.pageX, event.pageY);
        }

        document.addEventListener('mousemove', onMouseMove);

        el.onmouseup = function () {
            document.removeEventListener('mousemove', onMouseMove);
            el.onmouseup = null;
            saveWindowState();
        };
    };

    el.ondragstart = function () {
        return false;
    };
}

function toggleCollapse() {
    isCollapsed = !isCollapsed;

    const collapseBtn = document.querySelector('#collapse-btn i');
    const floatingWindow = document.getElementById('floating-window');

    if (isCollapsed) {
        if (!floatingWindow.dataset.originalHeight) {
            floatingWindow.dataset.originalHeight = `${floatingWindow.offsetHeight}px`;
        }


        floatingWindow.style.height = '40px';
        floatingWindow.style.overflow = 'hidden';


        if (collapseBtn) collapseBtn.className = 'el-icon-plus';
    } else {

        floatingWindow.style.height = floatingWindow.dataset.originalHeight || '';
        floatingWindow.style.overflow = '';

        if (collapseBtn) collapseBtn.className = 'el-icon-minus';
    }


    saveWindowState();
}

//=============================================

enableWebpackHook();

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const config = {
    awaitTime: 5000,
    stopTimer: false,
    questionCount: 0,
    finishCount: 0,
    questionType: {
        '判断题': 10,
        '单选题': 20,
        '多选题': 25,
        '填空题': 30,
        '问答题': 40,
    },
    apiKey: GM_getValue('apiKey', '')
};


function updateLogDisplay() {
    const logContainer = document.getElementById('log-container');
    if (!logContainer) return;

    logContainer.innerHTML = '';

    logMessages.forEach(entry => {
        const logItem = document.createElement('div');
        logItem.className = `log-item log-${entry.type}`;
        logItem.innerHTML = `
            <span class="log-timestamp">${entry.timestamp}</span>
            <span class="log-content">${entry.message}</span>
        `;
        logContainer.appendChild(logItem);
    });

    logContainer.scrollTop = logContainer.scrollHeight;
}

function answerQuestion(questionBody, questionIndex) {
    // 获取题目
    const questionTitle = questionBody.querySelector('.subject_describe div,.smallStem_describe p')
        .__Ivue__._data.shadowDom.textContent
        .replace(/%C2%A0/g, '%20')
        .replace(/\s+/g, ' ');

    console.log(`第 ${questionIndex} 题:`, questionTitle);

    // 获取选项
    const options = questionBody.querySelectorAll(".node_detail");
    if (options.length) {
        options.forEach((option, idx) => {
            console.log(`选项 ${idx + 1}:`, option.innerText.trim());
        });
    } else {
        console.log('未检测到选项');
    }

    appendToTable(questionTitle, "", questionIndex);
    logMessage(`正在处理第 ${questionIndex} 题: ${questionTitle.substring(0, 30)}...`);

    const questionType = questionBody.querySelector(".subject_type").innerText.match(/【(.+)】|$/)[1];
    let type = config.questionType[questionType];
    type = type !== undefined ? type : -1;

const apiurl = `https://www.fengtingwanli.cn/answer.php?question=${encodeURIComponent(questionTitle)}${config.apiKey ? `&key=${config.apiKey}` : ''}`;
console.log(apiurl)
GM_xmlhttpRequest({
    method: "GET",
    url: apiurl,
    onload: xhr => {
        try {
            const res = JSON.parse(xhr.responseText);
            const msg = res.msg;
            const answerString = res.answer;

            const questionTypeText = questionBody.querySelector(".subject_type").innerText.match(/【(.+)】|$/)[1];
            let type = config.questionType[questionTypeText];
            type = type !== undefined ? type : -1;

            if (msg === "暂无答案") {
                updateAnswerInTable(questionIndex, "暂无答案", false);
                logMessage(`第 ${questionIndex} 题: 暂无答案`, 'warning');
            } else if (msg === "无效的API,请检查后重试") {
                updateAnswerInTable(questionIndex, "API Key无效,请检查后重试", false);
                logMessage(`第 ${questionIndex} 题: API Key无效`, 'error');
            } else {
                updateAnswerInTable(questionIndex, answerString, true);
                logMessage(`第 ${questionIndex} 题: 已获取答案`, 'success');

                // **这里添加调用选择答案**
                const selected = chooseAnswer(type, questionBody, answerString);
                if (!selected) {
                    logMessage(`第 ${questionIndex} 题: 未匹配到网页选项,需要手动选择`, 'warning');
                }
            }

            // 自动切换下一题
            document.querySelectorAll('.switch-btn-box > button')[1]?.click();
        } catch (error) {
            logMessage(`第 ${questionIndex} 题: 解析答案出错 - ${error.message}`, 'error');
        }
    }
});
}
function chooseAnswer(questionType, questionBody, answerString) {
    let isSelect = false;
    const answers = answerString.split(/[\u0001,#=;=-|;、,]+/).map(a => a.trim()).filter(Boolean);

    if (!questionBody) return isSelect;

    switch (questionType) {
        case 10: // 判断题
            return handleJudgment(questionBody, answers);
        case 20: // 单选题
            return handleSingleChoice(questionBody, answers);
        case 25: // 多选题
            return handleMultipleChoice(questionBody, answers);
        case 30: // 填空题
            return handleFillInBlank(questionBody, answers);
        case 40: // 问答题
            return handleEssay(questionBody, answerString);
        default:
            return isSelect;
    }
}

// 判断题
function handleJudgment(questionBody, answers) {
    const firstOption = questionBody.querySelector(".nodeLab");
    const secondOption = questionBody.querySelectorAll(".nodeLab")[1];
    if (!firstOption || !secondOption) return false;

    const optionText = questionBody.querySelector(".node_detail")?.innerText || "";
    const givenAnswer = answers[0]?.toLowerCase();

    const isCorrectAnswer = /正确|是|对|√|t|true/i.test(givenAnswer);
    const isOptionCorrect = /正确|是|对|√|t|true/i.test(optionText);

    if (isCorrectAnswer === isOptionCorrect) {
        firstOption.click();
    } else if (!isOptionCorrect && !isCorrectAnswer) {
        firstOption.click();
    } else if (isCorrectAnswer !== isOptionCorrect) {
        secondOption.click();
    } else {
        return false;
    }

    return true;
}

function handleSingleChoice(questionBody, answers) {
    const options = questionBody.querySelectorAll(".node_detail");
    if (!options.length) return false;

    const targetAnswer = answers[0];
    if (!targetAnswer) return false;

    for (let i = 0; i < options.length; i++) {
        const optionText = options[i].innerText.trim();
        if (optionText === targetAnswer){
            clickOption(i, questionBody);
            return true;
        }
    }

    return false; 
}

function handleMultipleChoice(questionBody, answers) {
    const options = questionBody.querySelectorAll(".node_detail");
    if (!options.length || !answers.length) return false;

    let matched = false;
    const selectedOptions = new Set();

    answers.forEach(answer => {
        for (let i = 0; i < options.length; i++) {
            if (selectedOptions.has(i)) continue;
            const optionText = options[i].innerText.trim();
            if (optionText === answer) {
                clickOption(i, questionBody);
                selectedOptions.add(i);
                matched = true;
                break;
            }
        }
    });

    return matched;
}

function handleFillInBlank(questionBody, answers) {
    const blanks = questionBody.querySelectorAll(".blankInput");
    if (!blanks.length) return false;

    for (let i = 0; i < blanks.length; i++) {
        const blank = blanks[i];
        if (i < answers.length) {
            blank.value = answers[i];
        } else {

            blank.value = answers[0] || "";
        }
    }
    return blanks.length > 0;
}

function handleEssay(questionBody, answerString) {
    const answerArea = questionBody.querySelector("textarea");
    if (answerArea) {
        answerArea.value = answerString;
        return true;
    }
    return false;
}

function clickOption(index, questionBody) {
    const optionLabel = questionBody.querySelectorAll(".nodeLab")[index];
    if (optionLabel) optionLabel.click();
}

function appendToTable(questionTitle, answerString, questionIndex) {
    const table = document.querySelector("#record-table tbody");
    table.innerHTML += `<tr><td>${questionIndex}</td><td>${questionTitle}</td><td id="answer${questionIndex}">正在搜索...</td></tr>`;
}

function changeAnswerInTable(answerString, questionIndex, isSelect) {
    const answerCell = document.querySelector(`#answer${questionIndex}`);
    answerCell.innerHTML = answerString;
    if (answerString === "暂无答案") {
        answerCell.insertAdjacentHTML('beforeend', `<p style="color:red"><i class="el-icon-error"></i> 暂无答案</p>`);
    }
    if (!isSelect) {
        answerCell.insertAdjacentHTML('beforeend', `<p style="color:red"><i class="el-icon-warning"></i> 未匹配答案,请根据搜索结果手动选择答案</p>`);
    }
}

function enableWebpackHook() {
    const originCall = Function.prototype.call;
    Function.prototype.call = function (...args) {
        const result = originCall.apply(this, args);
        if (args[2]?.default?.version === '2.5.2') {
            args[2]?.default?.mixin({
                mounted: function () {
                    this.$el['__Ivue__'] = this;
                }
            });
        }
        return result;
    }
}

function updateAnswerInTable(questionIndex, msg, isSelect = true) {
    const answerCell = document.querySelector(`#answer${questionIndex}`);
    if (!answerCell) return;

    answerCell.innerHTML = '';
    const mainMsg = document.createElement('div');
    mainMsg.textContent = msg;
    answerCell.appendChild(mainMsg);

    if (!isSelect) {
        const warn = document.createElement('p');
        warn.style.color = 'red';
        warn.innerHTML = '<i class="el-icon-warning"></i> 未匹配答案,请根据搜索结果手动选择';
        answerCell.appendChild(warn);
        logMessage(`第 ${questionIndex} 题: 未匹配答案,请根据搜索结果手动选择`, 'warning');
    }


    const tbody = answerCell.closest('tbody');
    if (tbody) tbody.scrollTop = tbody.scrollHeight;
}



function saveApiKey() {
    const apiKeyInput = document.getElementById('api-key-input');
    const newApiKey = apiKeyInput.value.trim();
    if (newApiKey) {
        GM_setValue('apiKey', newApiKey);
        config.apiKey = newApiKey;
        logMessage('API Key 保存成功', 'success');
        showTip('API Key 保存成功', 'success');
    } else {
        logMessage('API Key 不能为空', 'warning');
        showTip('API Key 不能为空', 'warning');
    }
}

function clearApiKey() {
    GM_setValue('apiKey', '');
    config.apiKey = '';
    document.getElementById('api-key-input').value = '';
    showTip('API Key 已清除', 'info');
}

unsafeWindow.onload = (() => (async () => {

const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css';
document.head.appendChild(link);

    // ================== CSS  ==================
        GM_addStyle(`
#collapse-btn {cursor: pointer;}
.el-dialog {
    width: 500px;
    height: 400px;
    border-radius: 10px;
    box-shadow: 0 8px 25px rgba(0,0,0,0.15);

}
.el-dialog__header {
    background: linear-gradient(90deg, #409EFF, #66b1ff)!important;
    color: white;
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 16px;
    cursor: move;
}

.nav-bar {
    display: flex;
    background-color: #ea96aa;
    border-bottom: 1px solid #dcdfe6;
}
.nav-bar button {
    flex: 1;
    border: none;
    background: transparent;
    padding: 10px 0;
    font-size: 14px;
    cursor: pointer;
    transition: background 0.2s, color 0.2s;
}
.nav-bar button:hover { background-color: #e6f0ff; }
.nav-bar button.active {
    background-color: #409EFF;
    color: white;
    border-radius: 4px 4px 0 0;
}

.page {
    display: none;
    height: calc(100% - 100px);
    overflow-y: auto;
    box-sizing: border-box;
}

.page.active {
    display: block;
}

#record-table {
    width: 100%;
    table-layout: auto;
    border-collapse: collapse;
}
#record-table th, #record-table td {
    padding: 10px;
    text-align: center;
    vertical-align: middle;
    border: 1px solid #ebeef5;
    white-space: normal;
}
#record-table th:nth-child(1),
#record-table td:nth-child(1) { width: 10%; }
#record-table th:nth-child(2),
#record-table td:nth-child(2) { width: 60%; }
#record-table th:nth-child(3),
#record-table td:nth-child(3) { width: 30%; }

.el-card {
    box-shadow: 0 4px 12px rgba(0,0,0,0.05);
    margin: 0;
    background-color: #d4dbca!important;
}

.el-card__header {
    background-color: #f5f7fa;
    font-weight: 500;
    display: flex;
    align-items: center;
}

.el-card__header i {
    margin: 0;
    color: #409EFF;
}

.el-card__body {
    padding: 0;
    box-sizing: border-box;
}


.el-table {
    border-radius: 6px;
    width: 100%;
    border-collapse: collapse;
    table-layout: auto;  /* 内容撑开列宽 */
}

.el-table th, .el-table td {
    padding: 10px 12px;
    font-size: 14px;
    border-bottom: 1px solid #ebeef5;
    text-align: center;
    vertical-align: middle;
    white-space: normal;
}

.el-table th {
    background-color: #f5f7fa;
    font-weight: 600;
}



#log-container {
    font-size: 13px;
    overflow-y: auto;
    background: #f9fafc;
    padding: 0;
    line-height: 1.5;
}
.log-item { margin-bottom: 5px; }
.log-info { color: #909399; }
.log-success { color: #67c23a; }
.log-warning { color: #e6a23c; }
.log-error { color: #f56c6c; }

/* 输入框和按钮 */
.el-input__inner {
    height: 36px;
    line-height: 36px;
    border-radius: 6px;
    box-sizing: border-box;
}
.el-button--medium { padding: 6px 12px; font-size: 13px; border-radius: 4px; }
.el-button--primary { background-color: #409EFF; border-color: #409EFF; }
.el-button--primary:hover { background-color: #66b1ff; border-color: #66b1ff; }
.el-button--danger { background-color: #f56c6c; border-color: #f56c6c; }
.el-button--danger:hover { background-color: #f78989; }

/* 二维码卡片居中显示 */
.qr-code-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}
.qr-code-container img {
    width: auto;
    max-width: 200px;
    height: auto;
    margin: 0 auto;
    border-radius: 6px;
    border: 1px solid #ebeef5;
}
.qr-code-container p {
    font-size: 12px;
    color: #606266;
    margin-top: 8px;
}
    `);

    const dialogStyle = `
    <div id="floating-window" class="el-dialog">
        <div class="el-dialog__header">
            <span class="el-dialog__title">
                <i class="el-icon-lightning" style="color: #ffd700;"></i> 智慧树小助手
            </span>
            <span id="collapse-btn"><i class="el-icon-minus"></i></span>
        </div>

        <div class="nav-bar">
            <button class="nav-btn active" data-page="log-page">首页</button>
            <button class="nav-btn" data-page="qa-page">答题</button>
            <button class="nav-btn" data-page="api-page">API</button>
            <button class="nav-btn" data-page="qr-page">联系我</button>
        </div>

        <div class="page active" id="log-page">
            <div class="el-card">
                <div class="el-card__header"><i class="el-icon-document"></i> 操作日志</div>
                <div class="el-card__body"><div id="log-container"></div></div>
            </div>
        </div>

        <div class="page" id="qa-page">
            <div class="el-card">
                <div class="el-card__header"><i class="el-icon-document"></i> 答题记录</div>
                <div class="el-card__body">
                    <div class="el-table__body-wrapper">
                        <table id="record-table" class="el-table__body">
                            <thead>
                                <tr class="el-table__header">
                                    <th><div class="cell">序号</div></th>
                                    <th><div class="cell">题目</div></th>
                                    <th><div class="cell">答案</div></th>
                                </tr>
                            </thead>
                            <tbody></tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>

        <div class="page" id="api-page">
            <div class="el-card">
                <div class="el-card__header"><i class="el-icon-key"></i> API设置</div>
                <div class="el-card__body">
                    <div class="el-input el-input--medium">
                        <input id="api-key-input" type="text" placeholder="请输入API Key" class="el-input__inner" value="${config.apiKey}">
                    </div>
                    <div style="margin-top: 8px;">
                        <button id="save-api-key-btn" class="el-button el-button--primary el-button--medium"><i class="el-icon-check"></i> 保存</button>
                        <button id="clear-api-key-btn" class="el-button el-button--danger el-button--medium" style="margin-left:10px"><i class="el-icon-delete"></i> 清除</button>
                    </div>
                </div>
            </div>
        </div>

        <div class="page" id="qr-page">
            <div class="el-card qr-code-container">
                <div class="el-card__header"><i class="el-icon-mobile-phone"></i> 扫码联系我更新题库</div>
                <div class="el-card__body">
                    <img src="https://www.fengtingwanli.cn/wp-content/uploads/2024/10/1730359368-4bbfc02656cc0da1a6b2d0481a89ff7.jpg" alt="联系我">
                    <p>联系我更新题库</p>
                </div>
            </div>
        </div>
    </div>
    `;

document.body.insertAdjacentHTML('beforeend', dialogStyle);

makeElementResizableDraggable(document.getElementById('floating-window'));

document.querySelectorAll('.nav-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById(btn.dataset.page).classList.add('active');
});
});


    document.getElementById('collapse-btn').addEventListener('click', toggleCollapse);
    document.getElementById('save-api-key-btn').addEventListener('click', saveApiKey);
    document.getElementById('clear-api-key-btn').addEventListener('click', clearApiKey);


    logMessage('智慧树小助手已启动', 'info');
    logMessage('填写API后即可开始答题', 'info');
    await sleep(config.awaitTime);

    const questionBodyAll = document.querySelectorAll(".examPaper_subject.mt20");
    if (questionBodyAll.length === 0) {
        logMessage('未检测到题目', 'warning');
        return;
    }

    config.questionCount = questionBodyAll.length;
    logMessage(`共检测到 ${config.questionCount} 道题目`, 'info');

    answerQuestion(questionBodyAll[0], 1);
    let finishCount = 1;
    const interval = setInterval(() => {
        if (finishCount < questionBodyAll.length) {
            answerQuestion(questionBodyAll[finishCount], finishCount + 1);
            finishCount += 1;
        } else {
            clearInterval(interval);
            logMessage("所有题目已处理完成!", 'success');
        }
    }, 3000);
}))();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址