🧠AI智慧树知到自动答题助手🧠

AI自动答题,可暂停/重试,复制题目,模型自定义,窗口缩放,显示完整AI思考过程和回复。支持OpenAI, DeepSeek, Gemini等API。

// ==UserScript==
// @name        🧠AI智慧树知到自动答题助手🧠
// @namespace    https://gf.qytechs.cn/
// @description  AI自动答题,可暂停/重试,复制题目,模型自定义,窗口缩放,显示完整AI思考过程和回复。支持OpenAI, DeepSeek, Gemini等API。
// @author       AI Copilot
// @match        *://*.zhihuishu.com/stuExamWeb*
// @connect      *
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_getValue
// @resource css https://unpkg.com/[email protected]/dist/css/bootstrap.min.css
// @license      MIT
// @version 0.0.1.20250522155626
// ==/UserScript==

enableWebpackHook();

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

// --- 全局状态和配置 ---
let IS_PAUSED = false;
let CURRENT_PROCESSING_QUESTIONS = []; // 存储当前批次题目的详细信息
let IS_WINDOW_MINIMIZED = false;

const MAX_AI_RETRIES = 2;
const AI_RETRY_DELAY = 2000;
const DOCUMENTATION_URL = "https://gf.qytechs.cn/zh-CN/scripts/your-script-id-or-page"; // 【重要】替换为你的实际文档链接

const AI_PROVIDERS = {
    OPENAI: { name: "OpenAI", defaultUrl: "https://api.openai.com/v1/chat/completions", defaultModel: "gpt-3.5-turbo", getRequestData: (model, promptContent) => ({ model: model, messages: [{ role: "user", content: promptContent }], temperature: 0.1, max_tokens: 250 }), parseResponse: (res) => { if (res.choices && res.choices.length > 0) { const fullText = res.choices[0].message.content.trim(); const cleanAnswer = fullText.replace(/<think>[\s\S]*?<\/think>\s*/gi, "").trim(); return { fullText, cleanAnswer }; } return null; }, parseError: (res) => res.error ? res.error.message : null },
    DEEPSEEK: { name: "DeepSeek", defaultUrl: "https://api.deepseek.com/chat/completions", defaultModel: "deepseek-chat", getRequestData: (model, promptContent) => ({ model: model, messages: [{ role: "user", content: promptContent }], temperature: 0.1, max_tokens: 250 }), parseResponse: (res) => { if (res.choices && res.choices.length > 0) { const fullText = res.choices[0].message.content.trim(); const cleanAnswer = fullText.replace(/<think>[\s\S]*?<\/think>\s*/gi, "").trim(); return { fullText, cleanAnswer }; } return null; }, parseError: (res) => res.error ? res.error.message : null },
    GEMINI: { name: "Google Gemini", defaultUrl: "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.0-pro:generateContent", defaultModel: "gemini-1.0-pro", getRequestData: (model, promptContent) => ({ contents: [{ parts: [{ text: promptContent }] }], generationConfig: { temperature: 0.1, maxOutputTokens: 250, } }), parseResponse: (res) => { if (res.candidates && res.candidates.length > 0 && res.candidates[0].content && res.candidates[0].content.parts && res.candidates[0].content.parts.length > 0) { const fullText = res.candidates[0].content.parts[0].text.trim(); const cleanAnswer = fullText.replace(/<think>[\s\S]*?<\/think>\s*/gi, "").trim(); return { fullText, cleanAnswer }; } return null; }, parseError: (res) => res.error ? (res.error.message + (res.error.details ? ` Details: ${JSON.stringify(res.error.details)}` : '')) : null }
};

const config = { awaitTime: 2500, requestDelay: 3000, questionTypeMap: { '判断题': 'TrueFalse', '单选题': 'SingleChoice', '多选题': 'MultipleChoice', }, defaultQuestionTypeForAI: 'SingleChoice' };

// --- AI 调用核心函数 (与v2.9.1一致) ---
async function getAiAnswer(questionTypeKey, questionDescription, optionsText, questionIndex, retryCount = 0) { const selectedProviderKey = GM_getValue('selectedAiProvider', 'OPENAI'); const provider = AI_PROVIDERS[selectedProviderKey] || AI_PROVIDERS.OPENAI; let userAiUrl = GM_getValue(`aiApiUrl_${selectedProviderKey}`, provider.defaultUrl); const userAiKey = GM_getValue(`aiApiKey_${selectedProviderKey}`, ''); const userAiModel = GM_getValue(`aiModel_${selectedProviderKey}`, provider.defaultModel); if (!userAiUrl || (!userAiKey && provider !== AI_PROVIDERS.GEMINI) || (provider === AI_PROVIDERS.GEMINI && !userAiUrl.includes('key=') && !userAiKey) ) { const errorMsg = `AI配置错误: ${provider.name} API URL或Key未配置`; console.error(errorMsg); updateAiLog(questionIndex, errorMsg, "", {cleanAnswer: "AI配置错误", fullText: errorMsg}); return {cleanAnswer: "AI配置错误", fullText: errorMsg}; } if (provider === AI_PROVIDERS.GEMINI && userAiUrl.includes("{YOUR_API_KEY}") && userAiKey) { userAiUrl = userAiUrl.replace("{YOUR_API_KEY}", userAiKey); } else if (provider === AI_PROVIDERS.GEMINI && !userAiUrl.includes('key=') && userAiKey) { userAiUrl += (userAiUrl.includes('?') ? '&' : '?') + `key=${userAiKey}`; } let promptContent = `你是一个在线学习平台的答题助手。请根据以下题目信息,给出最准确的答案。\n\n题目类型:${questionTypeKey}\n题目描述:\n${questionDescription}\n`; if (optionsText && optionsText.trim() !== "") { promptContent += `\n选项:\n${optionsText}\n`; } promptContent += `\n请严格按照以下格式之一给出答案:\n- 对于单选题,请直接给出正确选项的字母(例如:A)。\n- 对于多选题,请直接给出所有正确选项的字母,用逗号分隔,无空格(例如:A,C,D)。\n- 对于判断题,请直接回答“正确”或“错误”。\n- 不要包含任何解释、题目复述或多余的文字,只需要答案本身。例如,如果答案是A,就只返回 "A"。如果是多选 A 和 C,就返回 "A,C"。\n`; if (retryCount > 0) { console.log(`[题目 ${questionIndex}] (${provider.name}) AI请求重试 #${retryCount}...\nPrompt:\n`, promptContent); updateAiLog(questionIndex, promptContent, `等待AI响应 (重试 ${retryCount}/${MAX_AI_RETRIES})...`, null); } else { console.log(`[题目 ${questionIndex}] (${provider.name}) Prompt:\n`, promptContent); updateAiLog(questionIndex, promptContent, "等待AI响应...", null); } const requestData = provider.getRequestData(userAiModel, promptContent); const headers = { "Content-Type": "application/json" }; if (provider !== AI_PROVIDERS.GEMINI) { headers["Authorization"] = `Bearer ${userAiKey}`; } return new Promise((resolve) => { GM_xmlhttpRequest({ method: "POST", url: userAiUrl, headers: headers, data: JSON.stringify(requestData), timeout: 25000, onload: function(response) { try { const res = JSON.parse(response.responseText); console.log(`[题目 ${questionIndex}] AI Raw Rsp:\n`, res); const parsed = provider.parseResponse(res); const errorMsgFromProvider = provider.parseError(res); if (parsed && parsed.cleanAnswer !== null) { const finalCleanAnswer = parsed.cleanAnswer.replace(/^["']/, '').replace(/["']$/, '').replace(/[。;,.]$/, '').trim(); updateAiLog(questionIndex, promptContent, JSON.stringify(res, null, 2), {cleanAnswer: finalCleanAnswer, fullText: parsed.fullText}); resolve({cleanAnswer: finalCleanAnswer, fullText: parsed.fullText}); } else if (errorMsgFromProvider) { const errorMsg = `AI API错误: ${errorMsgFromProvider}`; console.error(`[题目 ${questionIndex}] ${errorMsg}`); handleAiError(errorMsg, resolve, retryCount, questionIndex, promptContent, JSON.stringify(res, null, 2), { questionTypeKey, questionDescription, optionsText }); } else { const errorMsg = "AI未返回有效答案结构"; console.error(`[题目 ${questionIndex}] ${errorMsg}`); handleAiError(errorMsg, resolve, retryCount, questionIndex, promptContent, JSON.stringify(res, null, 2), { questionTypeKey, questionDescription, optionsText }); } } catch (e) { const errorMsg = `解析AI响应失败: ${e.message}`; console.error(`[题目 ${questionIndex}] ${errorMsg}`, response.responseText); handleAiError(errorMsg, resolve, retryCount, questionIndex, promptContent, response.responseText, { questionTypeKey, questionDescription, optionsText }); } }, onerror: function(error) { const errorMsg = `AI API请求错误: ${error.statusText || 'Network Error'}`; console.error(`[题目 ${questionIndex}] ${errorMsg}`, error); handleAiError(errorMsg, resolve, retryCount, questionIndex, promptContent, JSON.stringify(error, null, 2), { questionTypeKey, questionDescription, optionsText }); }, ontimeout: function() { const errorMsg = "AI API请求超时"; console.error(`[题目 ${questionIndex}] ${errorMsg}`); handleAiError(errorMsg, resolve, retryCount, questionIndex, promptContent, "请求超时", { questionTypeKey, questionDescription, optionsText }); } }); });}
async function handleAiError(errorMsg, resolve, currentRetryCount, questionIndex, promptContent, rawResponse, originalRequestArgs) { if (currentRetryCount < MAX_AI_RETRIES) { updateAiLog(questionIndex, promptContent, rawResponse, {cleanAnswer: `错误: ${errorMsg} (准备重试 ${currentRetryCount + 1})`, fullText: `错误: ${errorMsg} (准备重试 ${currentRetryCount + 1})`}); await sleep(AI_RETRY_DELAY); resolve(getAiAnswer(originalRequestArgs.questionTypeKey, originalRequestArgs.questionDescription, originalRequestArgs.optionsText, questionIndex, currentRetryCount + 1)); } else { updateAiLog(questionIndex, promptContent, rawResponse, {cleanAnswer: `错误: ${errorMsg} (已达最大重试次数)`, fullText: `错误: ${errorMsg} (已达最大重试次数)`}); resolve({cleanAnswer: `AI错误: ${errorMsg}`, fullText: `AI错误: ${errorMsg}`}); }}

// --- 更新AI日志到表格的详情区域 (与v2.9.1一致) ---
function updateAiLog(questionIndex, prompt, rawResponse, aiResult = null) { const logRow = document.querySelector(`#qrow-${questionIndex}-log`); if (logRow) { const promptCell = logRow.querySelector(".ai-prompt-cell"); const responseCell = logRow.querySelector(".ai-response-cell"); if (promptCell) { promptCell.textContent = prompt; } if (responseCell) { let displayText = `原始JSON响应:\n${rawResponse}\n\n`; if (aiResult && aiResult.fullText) { displayText += `AI完整回复 (含思考过程):\n${aiResult.fullText}\n\n`; displayText += `提取的纯净答案: ${aiResult.cleanAnswer !== null ? aiResult.cleanAnswer : "N/A"}`; } else if (aiResult && typeof aiResult === 'string') { displayText += `AI回复/错误: ${aiResult}`; } else { displayText += `AI未提供有效回复或提取结构。`; } responseCell.textContent = displayText; } } }

// --- 主答题逻辑 (与v2.9.1一致) ---
async function answerQuestionWithAI(questionTypeString, questionDescription, questionBody, questionIndex, isRetry = false) { if (!isRetry) { let questionTitle = questionDescription.trim().replace(/%C2%A0/g, '%20').replace(/\s+/g, ' '); appendToTable(questionTitle, "", questionIndex); } else { const answerCell = document.querySelector(`#ai-answer-${questionIndex}`); if (answerCell) answerCell.innerHTML = "正在重试AI..."; const retryButton = document.querySelector(`#retry-btn-${questionIndex}`); if (retryButton) retryButton.disabled = true; updateAiLog(questionIndex, "重试中...", "等待AI响应...", null); } const extractedTypeMatch = questionTypeString.match(/【(.+?)】/); const extractedType = extractedTypeMatch && extractedTypeMatch[1] ? extractedTypeMatch[1] : ''; const aiQuestionTypeKey = config.questionTypeMap[extractedType] || config.defaultQuestionTypeForAI; let optionsText = ""; const optionElements = questionBody.querySelectorAll(".subject_node .nodeLab .node_detail"); if (optionElements && optionElements.length > 0) { optionElements.forEach((opt, idx) => { const optionLabel = String.fromCharCode(65 + idx); let optText = opt.innerText.trim().replace(/^[A-Z][\s.、]*/, ''); if (aiQuestionTypeKey === 'TrueFalse') { optionsText += `${optText}\n`; } else { optionsText += `${optionLabel}. ${optText}\n`; } }); } else { console.warn(`[题目 ${questionIndex}] 未能使用选择器 ".subject_node .nodeLab .node_detail" 提取到选项元素。`); } optionsText = optionsText.trim(); console.log(`[题目 ${questionIndex}] (${isRetry ? '重试' : '首次'}): 类型=${extractedType}(${aiQuestionTypeKey}), 描述=${questionDescription.substring(0,30)}...`); if (!config.questionTypeMap[extractedType]) { const unsupportedMsg = "AI暂不支持此题型"; changeAnswerInTable(unsupportedMsg, questionIndex, false, `${unsupportedMsg},请手动完成。`, true); updateAiLog(questionIndex, "N/A (不支持的题型)", "N/A", {cleanAnswer: unsupportedMsg, fullText: unsupportedMsg}); return; } if (optionsText === "" && (aiQuestionTypeKey === 'SingleChoice' || aiQuestionTypeKey === 'MultipleChoice')) { console.warn(`[题目 ${questionIndex}] 选项提取为空,AI可能无法准确回答选择题。`); if (!isRetry) updateAiLog(questionIndex, "警告:选项提取为空。", "", {cleanAnswer: "警告", fullText: "警告:选项提取为空。AI将仅基于题干作答。"}); } const aiResult = await getAiAnswer(aiQuestionTypeKey, questionDescription, optionsText, questionIndex); const cleanAiAnswer = aiResult.cleanAnswer; console.log(`[题目 ${questionIndex}] AI最终提取答案: "${cleanAiAnswer}"`); let isSelected = false; let isError = cleanAiAnswer.startsWith("AI配置错误") || cleanAiAnswer.startsWith("AI错误") || cleanAiAnswer.startsWith("AI未返回") || cleanAiAnswer.startsWith("AI请求"); if (!isError) { isSelected = chooseAnswerByAI(aiQuestionTypeKey, questionBody, cleanAiAnswer); } changeAnswerInTable(cleanAiAnswer, questionIndex, isSelected, isSelected ? "" : (isError ? cleanAiAnswer : "AI答案可能未成功匹配选项或选择失败,请检查"), !isError || isSelected); if (!isRetry && !IS_PAUSED) { const nextButton = document.querySelector('.switch-btn-box > button:last-child'); if (nextButton && nextButton.innerText.includes('下一题')) { setTimeout(() => nextButton.click(), 500); } else { console.log("未找到明确的'下一题'按钮或已是最后一题。"); } } else if (isRetry) { const retryButton = document.querySelector(`#retry-btn-${questionIndex}`); if (retryButton) retryButton.disabled = false; } }

// --- 根据AI答案选择选项 (与v2.9.1一致) ---
function chooseAnswerByAI(aiQuestionTypeKey, questionBody, aiAnswerString) { let isSelectedSuccessfully = false; const cleanedAiAnswer = aiAnswerString.toUpperCase().replace(/\s+/g, ''); const clickableOptionElements = questionBody.querySelectorAll(".subject_node .nodeLab"); const optionDetailElements = questionBody.querySelectorAll(".subject_node .nodeLab .node_detail"); if (aiQuestionTypeKey === 'TrueFalse') { if (clickableOptionElements.length >= 1 && optionDetailElements.length >= 1) { let targetOptionIndex = -1; if (cleanedAiAnswer.includes("正确") || cleanedAiAnswer.includes("对") || cleanedAiAnswer.includes("T") || cleanedAiAnswer.includes("RIGHT")) { for(let i=0; i < optionDetailElements.length; i++){ const text = optionDetailElements[i].innerText.trim(); if(text.includes("正确") || text.includes("对")){ targetOptionIndex = i; break; } } if (targetOptionIndex === -1 && optionDetailElements.length > 0 && (optionDetailElements[0].innerText.trim().includes("正确") || optionDetailElements[0].innerText.trim().includes("对"))) targetOptionIndex = 0; else if (targetOptionIndex === -1) targetOptionIndex = 0; } else if (cleanedAiAnswer.includes("错误") || cleanedAiAnswer.includes("错") || cleanedAiAnswer.includes("F") || cleanedAiAnswer.includes("WRONG")) { for(let i=0; i < optionDetailElements.length; i++){ const text = optionDetailElements[i].innerText.trim(); if(text.includes("错误") || text.includes("错")){ targetOptionIndex = i; break; } } if (targetOptionIndex === -1 && optionDetailElements.length > 1 && (optionDetailElements[1].innerText.trim().includes("错误") || optionDetailElements[1].innerText.trim().includes("错"))) targetOptionIndex = 1; else if (targetOptionIndex === -1) targetOptionIndex = clickableOptionElements.length > 1 ? 1 : 0; } if(targetOptionIndex !== -1 && clickableOptionElements[targetOptionIndex]){ clickableOptionElements[targetOptionIndex].click(); if (clickableOptionElements[targetOptionIndex].querySelector('.node_detail')?.classList.contains('onChecked') || clickableOptionElements[targetOptionIndex].classList.contains('onChecked') || clickableOptionElements[targetOptionIndex].querySelector('input')?.checked) isSelectedSuccessfully = true; else { clickableOptionElements[targetOptionIndex].click(); if (clickableOptionElements[targetOptionIndex].querySelector('.node_detail')?.classList.contains('onChecked') || clickableOptionElements[targetOptionIndex].classList.contains('onChecked')) isSelectedSuccessfully = true; else console.warn(`[判断题 ${targetOptionIndex}] 点击后未能确认选中。`);} } } } else if (aiQuestionTypeKey === 'SingleChoice') { const targetOptionLetter = cleanedAiAnswer.charAt(0); if (targetOptionLetter >= 'A' && targetOptionLetter <= 'Z') { const optionIndex = targetOptionLetter.charCodeAt(0) - 'A'.charCodeAt(0); if (clickableOptionElements[optionIndex]) { clickableOptionElements[optionIndex].click(); if (clickableOptionElements[optionIndex].querySelector('.node_detail')?.classList.contains('onChecked') || clickableOptionElements[optionIndex].classList.contains('onChecked') || clickableOptionElements[optionIndex].querySelector('input')?.checked) isSelectedSuccessfully = true; else { clickableOptionElements[optionIndex].click(); if (clickableOptionElements[optionIndex].querySelector('.node_detail')?.classList.contains('onChecked') || clickableOptionElements[optionIndex].classList.contains('onChecked')) isSelectedSuccessfully = true; else console.warn(`[单选题 ${targetOptionLetter}] 点击后未能确认选中。`);} } } } else if (aiQuestionTypeKey === 'MultipleChoice') { const targetOptionsLetters = cleanedAiAnswer.split(',').map(s => s.trim()).filter(Boolean); let clickedCount = 0; targetOptionsLetters.forEach(letter => { if (letter >= 'A' && letter <= 'Z') { const optionIndex = letter.charCodeAt(0) - 'A'.charCodeAt(0); if (clickableOptionElements[optionIndex]) { clickableOptionElements[optionIndex].click(); clickedCount++; } } }); if (clickedCount === targetOptionsLetters.length && clickedCount > 0) isSelectedSuccessfully = true; else if (clickedCount > 0) { console.warn("多选题部分选项可能未匹配:", cleanedAiAnswer); isSelectedSuccessfully = true; } } return isSelectedSuccessfully; }

// --- UI 和辅助函数 ---
function truncateTitle(title) { if (title.length > 15) { return title.substring(0, 15) + '...'; } return title; } // 再短一点

function appendToTable(questionTitle, answerString, questionIndex) {
    const truncatedTitle = truncateTitle(questionTitle); const tableBody = document.querySelector("#ai-record-table tbody");
    if (tableBody) {
        const mainRow = document.createElement('tr'); mainRow.id = `qrow-${questionIndex}`;
        mainRow.innerHTML = `
            <td class="qa-actions"> ${questionIndex}
                <button class="btn btn-xs btn-default toggle-log-btn" data-q-idx="${questionIndex}" title="AI交互详情" style="padding:1px 3px; font-size:0.75em; margin-left:1px;">详</button>
                <button class="btn btn-xs btn-info copy-qa-btn" data-q-idx="${questionIndex}" title="复制题目和选项" style="padding:1px 3px; font-size:0.75em;">复</button>
            </td><td>${truncatedTitle}</td>
            <td id="ai-answer-${questionIndex}" style="min-width: 70px;">请求AI...</td>
            <td id="ai-action-${questionIndex}" style="width: 45px;"><button id="retry-btn-${questionIndex}" class="btn btn-xs btn-warning retry-btn" data-question-index="${questionIndex}" style="padding:1px 3px; font-size:0.75em; display:none;">重试</button></td>`;
        tableBody.appendChild(mainRow);
        const logRow = document.createElement('tr'); logRow.id = `qrow-${questionIndex}-log`; logRow.classList.add('ai-log-details'); logRow.style.display = 'none';
        logRow.innerHTML = `<td colspan="4" style="padding: 8px; background-color: #f9f9f9;"><div style="font-weight:bold; margin-bottom:5px;">AI交互详情 (题目 ${questionIndex}):</div><div style="margin-bottom:8px;"><strong style="color:#007bff;">Prompt:</strong><pre class="ai-prompt-cell" style="white-space: pre-wrap; word-break: break-all; max-height: 150px; overflow-y: auto; background-color: #eef; padding: 5px; border-radius: 3px; font-size:0.9em;"></pre></div><div><strong style="color:#28a745;">AI响应:</strong><pre class="ai-response-cell" style="white-space: pre-wrap; word-break: break-all; max-height: 250px; overflow-y: auto; background-color: #efe; padding: 5px; border-radius: 3px; font-size:0.9em;"></pre></div></td>`;
        tableBody.appendChild(logRow);
        mainRow.querySelector('.toggle-log-btn').addEventListener('click', function() { const logTargetId = `qrow-${this.dataset.qIdx}-log`; const targetRow = document.getElementById(logTargetId); if (targetRow) { targetRow.style.display = targetRow.style.display === 'none' ? 'table-row' : 'none'; this.textContent = targetRow.style.display === 'none' ? '详' : '收'; }}); // 改为单字
        mainRow.querySelector('.copy-qa-btn').addEventListener('click', function() { copyQuestionAndOptionsToClipboard(parseInt(this.dataset.qIdx)); });
        mainRow.querySelector('.retry-btn').addEventListener('click', function() { const qIndex = parseInt(this.getAttribute('data-question-index')); const questionData = CURRENT_PROCESSING_QUESTIONS.find(q => q.index === qIndex); if (questionData) { updateMsg(`正在重试第 ${qIndex} 题...`, "#007bff"); answerQuestionWithAI(questionData.type, questionData.description, questionData.body, qIndex, true); } });
    }
}

function copyQuestionAndOptionsToClipboard(questionIndex, all = false) {
    let textToCopy = "";
    if (all) {
        if (CURRENT_PROCESSING_QUESTIONS.length === 0) {
            GM_setClipboard("错误:没有题目信息可复制。"); updateMsg("复制失败:无题目信息。", "red"); return;
        }
        CURRENT_PROCESSING_QUESTIONS.forEach(qData => {
            textToCopy += `题目 ${qData.index}: ${qData.description.trim()}\n`;
            const optionElements = qData.body.querySelectorAll(".subject_node .nodeLab .node_detail");
            if (optionElements && optionElements.length > 0) {
                textToCopy += "选项:\n";
                optionElements.forEach((opt, idx) => {
                    const optionLabel = String.fromCharCode(65 + idx); const optText = opt.innerText.trim().replace(/^[A-Z][\s.、]*/, '');
                    const extractedTypeMatch = qData.type.match(/【(.+?)】/); const isTrueFalse = extractedTypeMatch && extractedTypeMatch[1] === '判断题';
                    if (isTrueFalse) { textToCopy += `${optText}\n`; } else { textToCopy += `${optionLabel}. ${optText}\n`; }
                });
            } else { textToCopy += "(无选项或选项提取失败)\n"; }
            textToCopy += "\n"; // 题目间空一行
        });
        GM_setClipboard(textToCopy.trim()); updateMsg(`全部 ${CURRENT_PROCESSING_QUESTIONS.length} 道题目及选项已复制!`, "green");
    } else {
        const questionData = CURRENT_PROCESSING_QUESTIONS.find(q => q.index === questionIndex);
        if (!questionData) { GM_setClipboard("错误:未找到题目信息。"); updateMsg("复制失败:未找到题目信息。", "red"); return; }
        textToCopy = `题目 ${questionIndex}: ${questionData.description.trim()}\n`;
        const optionElements = questionData.body.querySelectorAll(".subject_node .nodeLab .node_detail");
        if (optionElements && optionElements.length > 0) { textToCopy += "选项:\n"; optionElements.forEach((opt, idx) => { const optionLabel = String.fromCharCode(65 + idx); const optText = opt.innerText.trim().replace(/^[A-Z][\s.、]*/, ''); const extractedTypeMatch = questionData.type.match(/【(.+?)】/); const isTrueFalse = extractedTypeMatch && extractedTypeMatch[1] === '判断题'; if (isTrueFalse) { textToCopy += `${optText}\n`; } else { textToCopy += `${optionLabel}. ${optText}\n`; } }); }
        else { textToCopy += "(无选项或选项提取失败)\n"; }
        GM_setClipboard(textToCopy); updateMsg(`题目 ${questionIndex} 及选项已复制!`, "green");
    }
}

function changeAnswerInTable(answerString, questionIndex, isSelect, errorMessage, showRetry) { const answerCell = document.querySelector(`#ai-answer-${questionIndex}`); const retryButton = document.querySelector(`#retry-btn-${questionIndex}`); if (answerCell) { answerCell.innerHTML = answerString || "AI无回复"; if (errorMessage) { answerCell.insertAdjacentHTML('beforeend', `<div style="color:#ff8c00; font-size:0.85em; margin-top:2px;">${errorMessage}</div>`); } else if (!isSelect && answerString && !answerString.startsWith("AI")) { answerCell.insertAdjacentHTML('beforeend', `<div style="color:#dc3545; font-size:0.85em; margin-top:2px;">未匹配选项</div>`); } else if (isSelect){ answerCell.style.color = "#28a745"; answerCell.style.fontWeight = "bold"; } } if (retryButton) { retryButton.style.display = showRetry ? 'inline-block' : 'none'; if (!showRetry) retryButton.disabled = false; } }
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 () { if (this.$el && typeof this.$el === 'object') { this.$el['__Ivue__'] = this; } } }); } return result; }}
function makeElementDraggable(el) { el.style.position = 'fixed'; let shiftX, shiftY; const header = el.querySelector('.panel-heading'); if (!header) { console.warn("Draggable header not found for element:", el); return; } header.style.cursor = 'grab'; header.onmousedown = function(event) { if (event.target.closest('input, button, select, .panel-body-content, .table-panel-body, .ai-log-details pre, .window-controls button')) { return; } event.preventDefault(); header.style.cursor = 'grabbing'; shiftX = event.clientX - el.getBoundingClientRect().left; shiftY = event.clientY - el.getBoundingClientRect().top; el.style.zIndex = 100000; function moveAt(pageX, pageY) { let newLeft = pageX - shiftX; let newTop = pageY - shiftY; const rightEdge = window.innerWidth - el.offsetWidth; const bottomEdge = window.innerHeight - el.offsetHeight; if (newLeft < 0) newLeft = 0; if (newTop < 0) newTop = 0; if (newLeft > rightEdge) newLeft = rightEdge; if (newTop > bottomEdge) newTop = bottomEdge; el.style.left = newLeft + 'px'; el.style.top = newTop + 'px'; } function onMouseMove(event) { moveAt(event.pageX, event.pageY); } function onMouseUp() { header.style.cursor = 'grab'; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; el.ondragstart = () => false; }
function updateMsg(msg, color = '#007bff') { const displayMsgEl = document.getElementById('ai-display-msg'); if (displayMsgEl) { displayMsgEl.innerText = msg; displayMsgEl.style.color = color; } }

// --- 脚本主逻辑 ---
unsafeWindow.onload = (() => (async () => {
    console.log("AI答题脚本初始化..."); GM_addStyle(GM_getResourceText("css")); GM_addStyle(` #ai-floating-window { box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px; border: 1px solid #dee2e6; background-color: #fff;} #ai-floating-window .panel-heading { background-color: #007bff; color: white; border-top-left-radius: 7px; border-top-right-radius: 7px; padding: 8px 10px; cursor: grab; display: flex; justify-content: space-between; align-items: center; } #ai-floating-window .panel-title { font-size: 1.0em; font-weight: 600; margin:0; } #ai-floating-window .panel-body-content { padding: 10px; background-color: #f8f9fa; border-bottom: 1px solid #dee2e6; } #ai-floating-window label { font-size: 0.8em; margin-bottom: 1px; color: #495057; display: block;} #ai-floating-window .form-control, #ai-floating-window select { font-size: 0.85em; height: calc(1.4em + .5rem + 2px); padding: .2rem .4rem; margin-bottom: 6px; border-radius: 3px; width: 100%; box-sizing: border-box;} #ai-floating-window .btn { font-size: 0.85em; padding: 4px 7px; border-radius: 3px; margin-right: 4px;} #ai-display-msg { font-size: 0.8em; min-height: 16px; margin-top: 4px; font-weight: 500; } #ai-floating-window .table-panel-body { max-height: 150px; overflow-y: auto; padding: 0px; background-color: #ffffff; border-bottom-left-radius: 7px; border-bottom-right-radius: 7px;} #ai-record-table { font-size: 0.75em; margin-bottom: 0; table-layout: fixed; width:100%;} #ai-record-table th, #ai-record-table td { padding: 3px 5px; vertical-align: middle; word-break: break-word; } #ai-record-table th { background-color: #e9ecef; border-top: none !important; text-align: center;} #ai-record-table td.qa-actions { text-align: center; } #ai-record-table td:nth-child(2) { text-align: left; } #ai-record-table tr:last-child td { border-bottom: none; } .ai-log-details td { border-top: 1px dashed #ccc !important; } .toggle-log-btn, .copy-qa-btn, .retry-btn { vertical-align: middle; } #pause-resume-btn.paused { background-color: #28a745 !important; border-color: #28a745 !important; } .window-controls button { background: transparent; border: none; color: white; font-size: 1.1em; padding: 0 5px; line-height: 1; } `);
    let providerOptionsHTML = ""; for (const key in AI_PROVIDERS) { providerOptionsHTML += `<option value="${key}">${AI_PROVIDERS[key].name}</option>`; }
    const uiHTML = `
<div class="panel" id="ai-floating-window" style="position:fixed; left:15px; top:5%; width:320px; z-index:99999;">
  <div class="panel-heading">
    <h3 class="panel-title">🧠 AI答题助手 🧠</h3>
    <div class="window-controls">
        <button id="minimize-btn" title="最小化/恢复窗口">-</button>
    </div>
  </div>
  <div class="panel-content-wrapper"> <!-- 新增一个包装器用于隐藏/显示 -->
    <div class="panel-body-content">
        <div><label for="ai-provider-select">AI服务商:</label><select id="ai-provider-select" class="form-control">${providerOptionsHTML}</select></div>
        <div><label for="ai-api-url-input">API URL:</label><input id="ai-api-url-input" type="text" class="form-control" title="对应服务商的API接口地址"/></div>
        <div><label for="ai-model-input">模型名称:</label><input id="ai-model-input" type="text" class="form-control" title="自定义模型名称, 如gpt-4o"/></div>
        <div><label for="ai-api-key-input">API Key (当前服务商):</label><input id="ai-api-key-input" type="password" class="form-control" placeholder="输入API Key"/></div>
        <div style="display:flex; justify-content: space-around; align-items: center; margin-top:6px; flex-wrap: wrap;">
            <button id="save-ai-config-btn" class="btn btn-sm btn-info">保存配置</button>
            <button id="start-ai-autofill-btn" class="btn btn-sm btn-success">开始答题</button>
            <button id="docs-btn" class="btn btn-sm btn-secondary" title="查看使用说明">使用文档</button>
        </div>
        <div style="display:flex; justify-content: space-around; align-items: center; margin-top:4px; flex-wrap: wrap;">
            <button id="copy-all-qa-btn" class="btn btn-sm btn-primary" title="复制所有已处理的题目和选项" style="display:none;">复制全部</button>
            <button id="pause-resume-btn" class="btn btn-sm btn-warning" style="display:none;">暂停答题</button>
        </div>
        <div id="ai-display-msg" class="text-center">等待操作...</div>
    </div>
    <div class="table-panel-body"><table class="table table-bordered table-condensed" id="ai-record-table">
        <thead><tr><th style="width:22%;">#</th><th style="width:38%;">题目</th><th style="width:25%;">AI答案</th><th style="width:15%;">操作</th></tr></thead>
        <tbody></tbody>
    </table></div>
  </div>
</div>`;
    document.body.insertAdjacentHTML('beforeend', uiHTML); makeElementDraggable(document.getElementById('ai-floating-window'));
    const providerSelect = document.getElementById('ai-provider-select'); const apiUrlInput = document.getElementById('ai-api-url-input'); const apiKeyInput = document.getElementById('ai-api-key-input'); const modelInput = document.getElementById('ai-model-input'); const pauseResumeBtn = document.getElementById('pause-resume-btn'); const docsBtn = document.getElementById('docs-btn'); const minimizeBtn = document.getElementById('minimize-btn'); const panelContentWrapper = document.querySelector('#ai-floating-window .panel-content-wrapper'); const copyAllQaBtn = document.getElementById('copy-all-qa-btn');

    minimizeBtn.onclick = () => {
        IS_WINDOW_MINIMIZED = !IS_WINDOW_MINIMIZED;
        if (IS_WINDOW_MINIMIZED) {
            panelContentWrapper.style.display = 'none';
            minimizeBtn.textContent = '+'; // 或者用图标
            minimizeBtn.title = '恢复窗口';
        } else {
            panelContentWrapper.style.display = 'block';
            minimizeBtn.textContent = '-';
            minimizeBtn.title = '最小化窗口';
        }
    };

    copyAllQaBtn.onclick = () => { copyQuestionAndOptionsToClipboard(null, true); };

    function loadProviderConfig(providerKey) { const provider = AI_PROVIDERS[providerKey]; apiUrlInput.value = GM_getValue(`aiApiUrl_${providerKey}`, provider.defaultUrl); modelInput.value = GM_getValue(`aiModel_${providerKey}`, provider.defaultModel); modelInput.placeholder = `默认: ${provider.defaultModel}`; apiKeyInput.value = GM_getValue(`aiApiKey_${providerKey}`, ''); apiKeyInput.placeholder = `请输入 ${provider.name} 的 API Key`; }
    providerSelect.addEventListener('change', function() { GM_setValue('selectedAiProvider', this.value); loadProviderConfig(this.value); });
    const lastSelectedProvider = GM_getValue('selectedAiProvider', 'OPENAI'); providerSelect.value = lastSelectedProvider; loadProviderConfig(lastSelectedProvider);

    document.getElementById('save-ai-config-btn').onclick = () => { const selectedProviderKey = providerSelect.value; const provider = AI_PROVIDERS[selectedProviderKey]; GM_setValue(`aiApiUrl_${selectedProviderKey}`, apiUrlInput.value.trim()); GM_setValue(`aiApiKey_${selectedProviderKey}`, apiKeyInput.value.trim()); GM_setValue(`aiModel_${selectedProviderKey}`, modelInput.value.trim() || provider.defaultModel); GM_setValue('selectedAiProvider', selectedProviderKey); updateMsg(`${provider.name} 配置已保存!`, "#28a745"); };
    docsBtn.onclick = () => { window.open(DOCUMENTATION_URL, '_blank'); };
    pauseResumeBtn.onclick = () => { IS_PAUSED = !IS_PAUSED; if (IS_PAUSED) { pauseResumeBtn.textContent = "继续答题"; pauseResumeBtn.classList.add("paused"); updateMsg("答题已暂停。", "orange"); } else { pauseResumeBtn.textContent = "暂停答题"; pauseResumeBtn.classList.remove("paused"); updateMsg("答题已恢复,处理下一题...", "#007bff"); processNextQuestionFromQueue(); }};
    let questionQueue = []; let currentQuestionQueueIndex = 0;
    async function processNextQuestionFromQueue() { if (IS_PAUSED) return; if (currentQuestionQueueIndex < questionQueue.length) { const question = questionQueue[currentQuestionQueueIndex]; updateMsg(`处理中: ${question.index} / ${questionQueue.length} (总)`, "#007bff"); question.body.scrollIntoView({ behavior: 'smooth', block: 'center' }); await sleep(600); await answerQuestionWithAI(question.type, question.description, question.body, question.index); currentQuestionQueueIndex++; await sleep(config.requestDelay); processNextQuestionFromQueue(); } else { updateMsg(`所有 ${questionQueue.length} 题处理完毕!`, "#28a745"); document.getElementById('start-ai-autofill-btn').disabled = false; pauseResumeBtn.style.display = 'none'; copyAllQaBtn.style.display = 'inline-block'; const submitButton = document.querySelector('button.btn-submit, button.btn-handExam, input[type="button"][value="交卷"], a[onclick*="submitExam"], div.submit-btn'); if (submitButton) { alert("AI答题完成!请仔细检查答案后手动点击“交卷”或“提交”按钮。"); submitButton.style.outline = "3px solid red"; submitButton.style.transform = "scale(1.1)"; submitButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else { alert("AI答题完成!未找到明确的交卷按钮,请手动操作。"); } } }
    document.getElementById('start-ai-autofill-btn').onclick = async () => { const selectedProviderKey = providerSelect.value; const currentApiUrl = GM_getValue(`aiApiUrl_${selectedProviderKey}`); const currentApiKey = GM_getValue(`aiApiKey_${selectedProviderKey}`); const provider = AI_PROVIDERS[selectedProviderKey]; let apiKeyNeeded = true; if (provider === AI_PROVIDERS.GEMINI && currentApiUrl.toLowerCase().includes('key=')) { apiKeyNeeded = false; } if (!currentApiUrl || (apiKeyNeeded && !currentApiKey)) { updateMsg(`请先为 ${provider.name} 配置API URL${apiKeyNeeded ? '和Key':''}并保存!`, "#dc3545"); return; } updateMsg("开始AI自动答题...", "#007bff"); document.getElementById('start-ai-autofill-btn').disabled = true; pauseResumeBtn.style.display = 'inline-block'; pauseResumeBtn.textContent = "暂停答题"; pauseResumeBtn.classList.remove("paused"); IS_PAUSED = false; copyAllQaBtn.style.display = 'none'; document.querySelector("#ai-record-table tbody").innerHTML = ""; await sleep(config.awaitTime); const questionBodyAll = document.querySelectorAll(".examPaper_subject"); if (questionBodyAll.length === 0) { updateMsg("未检测到题目。", "#dc3545"); document.getElementById('start-ai-autofill-btn').disabled = false; pauseResumeBtn.style.display = 'none'; return; } CURRENT_PROCESSING_QUESTIONS = []; questionQueue = []; currentQuestionQueueIndex = 0;
        questionBodyAll.forEach((questionBody, index) => { const subjectNumElement = questionBody.querySelector(".subject_num.fl"); const questionNumberText = subjectNumElement ? subjectNumElement.textContent.trim() : `${index + 1}`; const questionTypeElement = questionBody.querySelector(".subject_type_annex") || questionBody.querySelector(".subject_type span:first-child"); const questionTypeStringFromDOM = questionTypeElement ? questionTypeElement.textContent.trim() : '【未知题型】'; let questionDescription = ''; let smallStemPElement = questionBody.querySelector(".smallStem_describe p") || questionBody.querySelector(".smallStem_describe"); if (smallStemPElement && typeof smallStemPElement.textContent === 'string') { questionDescription = smallStemPElement.textContent.trim(); } if (!questionDescription) { const descriptionDivs = questionBody.querySelectorAll(".subject_describe div"); let foundViaIvue = false; for (const div of descriptionDivs) { if (div.__Ivue__ && div.__Ivue__._data && typeof div.__Ivue__._data.shadowDom?.textContent === 'string' && div.__Ivue__._data.shadowDom.textContent.trim() !== "") { questionDescription = div.__Ivue__._data.shadowDom.textContent.trim(); foundViaIvue = true; break; } } if (!foundViaIvue && descriptionDivs.length > 0 && typeof descriptionDivs[0].textContent === 'string') { questionDescription = descriptionDivs[0].textContent.trim(); } } if (!questionDescription) { const subjectDescribeElement = questionBody.querySelector(".subject_describe"); if (subjectDescribeElement && typeof subjectDescribeElement.innerText === 'string') { questionDescription = subjectDescribeElement.innerText.trim(); }} if (questionDescription) { questionDescription = questionDescription.replace(/^\d+\s*[\.、.]\s*/, '').trim(); if (questionTypeStringFromDOM && questionDescription.startsWith(questionTypeStringFromDOM)) { questionDescription = questionDescription.substring(questionTypeStringFromDOM.length).trim(); } const typeTextOnly = questionTypeStringFromDOM.replace(/[【】]/g, ""); if (typeTextOnly && questionDescription.startsWith(typeTextOnly)) { if (questionDescription.length > typeTextOnly.length && (questionDescription[typeTextOnly.length] === ' ' || !isNaN(parseInt(questionDescription[typeTextOnly.length])) ) ) { questionDescription = questionDescription.substring(typeTextOnly.length).trim(); } } questionDescription = questionDescription.replace(/^【.*?】\s*/, '').trim().replace(/^(题目|题干)[::\s]*/, '').trim(); }
            if (questionDescription) { const questionData = { index: index + 1, type: questionTypeStringFromDOM, description: questionDescription, body: questionBody }; CURRENT_PROCESSING_QUESTIONS.push(questionData); questionQueue.push(questionData); }
            else { console.warn(`[题目 ${index + 1}] 描述提取失败。`); updateMsg(`警告: 第 ${index + 1} 题目描述提取失败。`, "#ffc107"); }
        });
        if (questionQueue.length === 0) { updateMsg("未能成功提取任何题目信息。", "#dc3545"); document.getElementById('start-ai-autofill-btn').disabled = false; pauseResumeBtn.style.display = 'none'; return; }
        updateMsg(`共 ${questionQueue.length} 题,开始处理...`, "#007bff"); processNextQuestionFromQueue();
    };
}))();

QingJ © 2025

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