晚风知到答题小助手

智慧树章节测试自动答题

在您安裝前,Greasy Fork希望您了解本腳本包含“負面功能”,可能幫助腳本的作者獲利,而不能給你帶來任何收益。

此腳本會在您造訪的網站插入廣告

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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