GoTest AI Auto Answer

AI自动答题脚本 - 支持Anthropic/OpenAI API

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @license MIT
// @name         GoTest AI Auto Answer
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  AI自动答题脚本 - 支持Anthropic/OpenAI API
// @match        https://dodo.hznu.edu.cn/GoTest/*
// @match        http://dodo.hznu.edu.cn/GoTest/*
// @match        *://*.hznu.edu.cn/*/GoTest/*
// @match        *://*.hznu.edu.cn/GoTest/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      *
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ========== 工具函数 ==========
    const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
    const jQuery = pageWindow.jQuery;
    const $ = jQuery;

    function escapeHtml(str) {
        const div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML;
    }
    function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

    // 下载图片转base64
    function fetchImageAsBase64(url) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET', url, responseType: 'blob', timeout: 15000,
                onload(resp) {
                    if (resp.status !== 200) { resolve(null); return; }
                    const reader = new FileReader();
                    reader.onloadend = () => {
                        const dataUrl = reader.result; // data:image/png;base64,...
                        resolve(dataUrl);
                    };
                    reader.onerror = () => resolve(null);
                    reader.readAsDataURL(resp.response);
                },
                onerror() { resolve(null); },
                ontimeout() { resolve(null); },
            });
        });
    }

    // ========== 配置系统 ==========
    const DEFAULT_CONFIG = {
        apiUrl: '',
        apiKey: '',
        model: 'claude-sonnet-4-20250514',
        apiFormat: 'openai',
        language: 'Python',
        batchSizeChoice: 20,
        batchSizeCode: 3,
        enableCodeQuestions: false,
        enableImageVision: false,
        apiTimeout: 120000,
        fetchDelay: 300,
    };

    function loadConfig() {
        try {
            const saved = GM_getValue('ai_config', null);
            return saved ? { ...DEFAULT_CONFIG, ...JSON.parse(saved) } : { ...DEFAULT_CONFIG };
        } catch { return { ...DEFAULT_CONFIG }; }
    }
    function saveConfig(cfg) { GM_setValue('ai_config', JSON.stringify(cfg)); }

    const CODE_TYPES = ['SHORT_ANSWER', 'PROGRAM_DESIGN', 'PROGRAM_CORRECT', 'DB_SQL_DESIGN', 'DESIGN'];
    function isCodeType(type) { return CODE_TYPES.includes(type); }

    // ========== 配置面板 UI ==========
    function showConfigPanel() {
        const cfg = loadConfig();
        const existing = document.getElementById('ai-config-panel');
        if (existing) { existing.remove(); document.getElementById('ai-config-overlay')?.remove(); return; }

        const overlay = document.createElement('div');
        overlay.id = 'ai-config-overlay';
        overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:99999;';
        overlay.addEventListener('click', () => { overlay.remove(); panel.remove(); });

        const panel = document.createElement('div');
        panel.id = 'ai-config-panel';
        Object.assign(panel.style, {
            position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
            zIndex: '100000', background: '#fff', borderRadius: '12px', padding: '24px',
            boxShadow: '0 8px 40px rgba(0,0,0,0.3)', width: '440px', fontFamily: 'sans-serif',
            maxHeight: '90vh', overflowY: 'auto',
        });
        const lbl = 'display:block;margin-bottom:10px;';
        const sp = 'font-size:13px;color:#666;';
        const inp = 'width:100%;padding:7px;margin-top:3px;border:1px solid #ddd;border-radius:6px;box-sizing:border-box;';
        panel.innerHTML = `
            <h3 style="margin:0 0 14px;font-size:18px;color:#333;">AI 答题配置</h3>
            <label style="${lbl}"><span style="${sp}">API 格式</span>
                <select id="cfg-format" style="${inp}">
                    <option value="openai" ${cfg.apiFormat==='openai'?'selected':''}>OpenAI 兼容</option>
                    <option value="anthropic" ${cfg.apiFormat==='anthropic'?'selected':''}>Anthropic</option>
                </select></label>
            <label style="${lbl}"><span style="${sp}">API URL</span>
                <input id="cfg-url" type="text" value="${escapeHtml(cfg.apiUrl)}" placeholder="https://api.openai.com" style="${inp}"></label>
            <label style="${lbl}"><span style="${sp}">API Key</span>
                <input id="cfg-key" type="password" value="${escapeHtml(cfg.apiKey)}" style="${inp}"></label>
            <label style="${lbl}"><span style="${sp}">模型名称</span>
                <input id="cfg-model" type="text" value="${escapeHtml(cfg.model)}" style="${inp}"></label>
            <label style="${lbl}"><span style="${sp}">编程语言 (影响代码题和填空题的回答)</span>
                <input id="cfg-lang" type="text" value="${escapeHtml(cfg.language)}" placeholder="Python / C++ / Java ..." style="${inp}"></label>
            <div style="display:flex;gap:10px;">
                <label style="${lbl}flex:1;"><span style="${sp}">选择/填空 每批题数</span>
                    <input id="cfg-batch-choice" type="number" value="${cfg.batchSizeChoice}" min="1" max="50" style="${inp}"></label>
                <label style="${lbl}flex:1;"><span style="${sp}">代码题 每批题数</span>
                    <input id="cfg-batch-code" type="number" value="${cfg.batchSizeCode}" min="1" max="10" style="${inp}"></label>
            </div>
            <label style="${lbl}"><span style="${sp}">API 超时 (秒)</span>
                <input id="cfg-timeout" type="number" value="${Math.round(cfg.apiTimeout/1000)}" min="30" max="600" style="${inp}"></label>
            <label style="${lbl}cursor:pointer;display:flex;align-items:center;gap:8px;">
                <input id="cfg-enable-code" type="checkbox" ${cfg.enableCodeQuestions?'checked':''} style="width:18px;height:18px;">
                <span style="${sp}">启用代码/简答题 AI 作答</span></label>
            <label style="${lbl}cursor:pointer;display:flex;align-items:center;gap:8px;">
                <input id="cfg-enable-image" type="checkbox" ${cfg.enableImageVision?'checked':''} style="width:18px;height:18px;">
                <span style="${sp}">启用图片识别 (需多模态模型,耗token较多)</span></label>
            <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:12px;">
                <button id="cfg-cancel" style="padding:8px 20px;border:1px solid #ddd;border-radius:6px;background:#fff;cursor:pointer;">取消</button>
                <button id="cfg-save" style="padding:8px 20px;border:none;border-radius:6px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;cursor:pointer;font-weight:bold;">保存</button>
            </div>`;

        document.body.appendChild(overlay);
        document.body.appendChild(panel);

        panel.querySelector('#cfg-save').addEventListener('click', () => {
            saveConfig({
                apiFormat: panel.querySelector('#cfg-format').value,
                apiUrl: panel.querySelector('#cfg-url').value.trim(),
                apiKey: panel.querySelector('#cfg-key').value.trim(),
                model: panel.querySelector('#cfg-model').value.trim(),
                language: panel.querySelector('#cfg-lang').value.trim() || 'Python',
                batchSizeChoice: parseInt(panel.querySelector('#cfg-batch-choice').value) || 20,
                batchSizeCode: parseInt(panel.querySelector('#cfg-batch-code').value) || 3,
                apiTimeout: (parseInt(panel.querySelector('#cfg-timeout').value) || 120) * 1000,
                enableCodeQuestions: panel.querySelector('#cfg-enable-code').checked,
                enableImageVision: panel.querySelector('#cfg-enable-image').checked,
                fetchDelay: cfg.fetchDelay,
            });
            overlay.remove(); panel.remove();
            log('配置已保存', 'success');
        });
        panel.querySelector('#cfg-cancel').addEventListener('click', () => { overlay.remove(); panel.remove(); });
    }

    // ========== 解除页面限制 ==========
    function unlockPage() {
        document.body.onselectstart = null;
        document.body.ondrag = null;
        document.oncontextmenu = null;
        window.onkeydown = null;
        document.onkeydown = null;
        if (typeof jQuery !== 'undefined') {
            jQuery(document).unbind('contextmenu');
            jQuery(document).unbind('keydown');
        }
        document.body.style.userSelect = 'auto';
        document.body.style.webkitUserSelect = 'auto';
        const style = document.createElement('style');
        style.textContent = '* { user-select: auto !important; -webkit-user-select: auto !important; }';
        document.head.appendChild(style);
        log('页面限制已解除');
    }

    // ========== 日志 & 进度 ==========
    let statusPanel = null;
    function createStatusPanel() {
        if (statusPanel) return;
        statusPanel = document.createElement('div');
        statusPanel.id = 'ai-status-panel';
        Object.assign(statusPanel.style, {
            position: 'fixed', bottom: '20px', right: '220px', width: '360px',
            minHeight: '60px', maxHeight: '500px', background: '#1a1a2e',
            color: '#0f0', fontFamily: 'Consolas, monospace', fontSize: '12px',
            borderRadius: '8px', zIndex: '99999', resize: 'both', overflow: 'hidden',
            boxShadow: '0 4px 20px rgba(0,0,0,0.5)', border: '1px solid #333',
            display: 'flex', flexDirection: 'column',
        });
        statusPanel.innerHTML = `
            <div id="ai-status-header" style="color:#0ff;font-weight:bold;padding:8px 12px;cursor:move;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
                <span>AI Auto Answer v2</span>
                <button id="ai-status-toggle" style="background:none;border:none;color:#0ff;font-size:16px;cursor:pointer;line-height:1;">−</button>
            </div>
            <div id="ai-status-body" style="flex:1;overflow-y:auto;padding:0 12px 12px;"><div id="ai-log"></div></div>`;
        document.body.appendChild(statusPanel);
        // 折叠
        statusPanel.querySelector('#ai-status-toggle').addEventListener('click', function () {
            const body = document.getElementById('ai-status-body');
            if (body.style.display === 'none') { body.style.display = ''; this.textContent = '−'; }
            else { body.style.display = 'none'; this.textContent = '+'; }
        });
        // 拖拽
        let dragging = false, dx = 0, dy = 0;
        const header = statusPanel.querySelector('#ai-status-header');
        header.addEventListener('mousedown', e => {
            if (e.target.id === 'ai-status-toggle') return;
            dragging = true; dx = e.clientX - statusPanel.offsetLeft; dy = e.clientY - statusPanel.offsetTop;
            statusPanel.style.bottom = 'auto'; statusPanel.style.right = 'auto';
        });
        document.addEventListener('mousemove', e => { if (!dragging) return; statusPanel.style.left = (e.clientX - dx) + 'px'; statusPanel.style.top = (e.clientY - dy) + 'px'; });
        document.addEventListener('mouseup', () => { dragging = false; });
    }
    function log(msg, type = 'info') {
        console.log(`[AI] ${msg}`);
        createStatusPanel();
        const logDiv = document.getElementById('ai-log');
        if (!logDiv) return;
        const colors = { info: '#0f0', warn: '#ff0', error: '#f44', success: '#0ff' };
        const line = document.createElement('div');
        line.style.color = colors[type] || '#0f0';
        line.style.marginBottom = '2px';
        line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
        logDiv.appendChild(line);
        logDiv.scrollTop = logDiv.scrollHeight;
    }
    function updateProgress(current, total, phase) {
        createStatusPanel();
        let bar = document.getElementById('ai-progress-bar');
        if (!bar) {
            bar = document.createElement('div');
            bar.id = 'ai-progress-bar';
            bar.style.cssText = 'margin:6px 0;background:#333;border-radius:4px;height:18px;position:relative;overflow:hidden;';
            bar.innerHTML = '<div id="ai-progress-fill" style="height:100%;background:linear-gradient(90deg,#00c6ff,#0072ff);border-radius:4px;transition:width 0.3s;"></div><span id="ai-progress-text" style="position:absolute;top:0;left:0;right:0;text-align:center;line-height:18px;font-size:11px;color:#fff;"></span>';
            const logDiv = document.getElementById('ai-log');
            if (logDiv) logDiv.parentElement.insertBefore(bar, logDiv);
        }
        const pct = Math.round((current / total) * 100);
        document.getElementById('ai-progress-fill').style.width = pct + '%';
        document.getElementById('ai-progress-text').textContent = `${phase}: ${current}/${total} (${pct}%)`;
    }

    // ========== Step 2: 抓取题目 ==========
    async function fetchAllQuestions() {
        const questionNodes = document.querySelectorAll('.bundle-item dd');
        const total = questionNodes.length;
        log(`发现 ${total} 道题目,开始抓取...`);
        const questions = [];
        for (let i = 0; i < total; i++) {
            const node = questionNodes[i];
            const qId = node.getAttribute('data-val-id');
            try {
                updateProgress(i + 1, total, '抓题');
                const resp = await fetch(`/GoTest/QuestionOne?id=${qId}&mode=4&tail=${Date.now()}`);
                const html = await resp.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                const questionItem = doc.querySelector('.question-item');
                if (!questionItem) { log(`第${i+1}题解析失败`, 'warn'); continue; }
                const qType = questionItem.getAttribute('data-type') || '';
                const questionFace = doc.querySelector('.question-face');
                const qText = questionFace ? questionFace.innerText.trim() : '';
                const images = [];
                const imageBase64 = [];
                if (questionFace) {
                    questionFace.querySelectorAll('img').forEach(img => {
                        const src = img.getAttribute('src');
                        if (src) {
                            if (src.startsWith('data:')) {
                                // 已经是 base64 data URI,直接作为 imageBase64 用
                                imageBase64.push(src);
                            } else {
                                images.push(src.startsWith('http') ? src : location.origin + src);
                            }
                        }
                    });
                }
                // 下载图片转base64(如果启用了图片识别)
                if (images.length > 0 && loadConfig().enableImageVision) {
                    for (const imgUrl of images) {
                        const b64 = await fetchImageAsBase64(imgUrl);
                        if (b64) imageBase64.push(b64);
                        else log(`题${i+1}: 图片下载失败 ${imgUrl}`, 'warn');
                    }
                }
                const q = { index: i + 1, id: qId, type: qType, text: qText, images, imageBase64, options: [] };
                if (['SINGLE_CHIOCE', 'MULIT_CHIOCE', 'JUDGE'].includes(qType)) {
                    const inputClass = qType === 'MULIT_CHIOCE' ? '.question-multi-input' : '.question-option-input';
                    doc.querySelectorAll(inputClass).forEach(input => {
                        const val = input.value || '';
                        const label = input.closest('label') || input.parentElement;
                        q.options.push({ value: val, text: label ? label.innerText.trim() : val });
                    });
                }
                if (['FILL_BLANK', 'PROGRAM_FILL_BLANK'].includes(qType)) {
                    q.blankCount = doc.querySelectorAll('.question-blank-input').length;
                }
                if (qType === 'COMPLEX') {
                    q.subQuestions = [];
                    doc.querySelectorAll('.sub-question').forEach((sub, si) => {
                        const subType = sub.getAttribute('data-type') || '';
                        const subText = sub.querySelector('.question-face')?.innerText.trim() || sub.innerText.substring(0, 200);
                        const subQ = { index: si + 1, type: subType, text: subText, options: [] };
                        if (['SINGLE_CHIOCE', 'MULIT_CHIOCE', 'JUDGE'].includes(subType)) {
                            const cls = subType === 'MULIT_CHIOCE' ? '.question-multi-input' : '.sub-question-option-input, .question-option-input';
                            sub.querySelectorAll(cls).forEach(input => {
                                const label = input.closest('label') || input.parentElement;
                                subQ.options.push({ value: input?.value || '', text: label?.innerText.trim() || '' });
                            });
                        }
                        if (subType === 'FILL_BLANK') subQ.blankCount = sub.querySelectorAll('.sub-question-blank-input').length;
                        q.subQuestions.push(subQ);
                    });
                }
                questions.push(q);
                log(`第${i+1}/${total}题 [${qType}] 抓取成功`, 'success');
            } catch (err) { log(`第${i+1}题请求失败: ${err.message}`, 'error'); }
            await sleep(loadConfig().fetchDelay);
        }
        log(`抓取完成,共 ${questions.length} 题`);
        return questions;
    }

    // ========== Step 3: AI API 调用 ==========
    // 构建prompt - 返回 { text, hasImages, contentParts }
    function buildPrompt(batch) {
        const cfg = loadConfig();
        const hasVisionImages = cfg.enableImageVision && batch.some(q => q.imageBase64?.length > 0);

        let textPrompt = `你是一个答题助手。请根据以下题目给出答案。
当前课程的编程语言是 ${cfg.language},所有代码题、程序填空题请使用 ${cfg.language} 作答。
严格按照JSON数组格式返回,不要有任何多余文字,只返回JSON。
格式:[{"id": "题目ID", "answer": "答案"}]
answer规则:
- 单选题(SINGLE_CHIOCE):返回选项字母,如 "A"
- 多选题(MULIT_CHIOCE):返回选项字母数组,如 ["A","C"]
- 判断题(JUDGE):返回 "对" 或 "错"
- 填空题(FILL_BLANK/PROGRAM_FILL_BLANK):返回数组,如 ["答案1","答案2"]
- 编程/简答/设计题:返回完整的 ${cfg.language} 代码或文字答案字符串
- 综合题(COMPLEX):answer为数组,每个元素对应一个子题的答案

题目列表:
`;
        // 如果有图片,构建多模态 content 数组
        if (hasVisionImages) {
            const parts = []; // content array for multimodal
            parts.push({ type: 'text', text: textPrompt });
            batch.forEach(q => {
                let qText = `\n--- 第${q.index}题 [ID:${q.id}] [类型:${q.type}] ---\n${q.text}\n`;
                if (q.options.length > 0) {
                    qText += '选项:\n';
                    q.options.forEach((o, j) => qText += `  ${String.fromCharCode(65 + j)}. ${o.text}\n`);
                }
                if (q.blankCount) qText += `(共${q.blankCount}个空)\n`;
                if (q.subQuestions) {
                    qText += '子题:\n';
                    q.subQuestions.forEach((sq, si) => {
                        qText += `  子题${si+1} [类型:${sq.type}]: ${sq.text}\n`;
                        sq.options?.forEach((o, j) => qText += `    ${String.fromCharCode(65 + j)}. ${o.text}\n`);
                    });
                }
                parts.push({ type: 'text', text: qText });
                // 插入图片
                if (q.imageBase64?.length > 0) {
                    q.imageBase64.forEach(dataUrl => {
                        const match = dataUrl.match(/^data:(image\/\w+);base64,(.+)$/);
                        if (!match) return;
                        const mediaType = match[1];
                        const b64data = match[2];
                        if (cfg.apiFormat === 'anthropic') {
                            parts.push({ type: 'image', source: { type: 'base64', media_type: mediaType, data: b64data } });
                        } else {
                            parts.push({ type: 'image_url', image_url: { url: dataUrl } });
                        }
                    });
                }
            });
            return { hasImages: true, contentParts: parts };
        }

        // 纯文本模式
        batch.forEach(q => {
            textPrompt += `\n--- 第${q.index}题 [ID:${q.id}] [类型:${q.type}] ---\n${q.text}\n`;
            if (q.images?.length > 0 && !cfg.enableImageVision) textPrompt += `(本题含${q.images.length}张图片,无法显示,请根据文字尽量作答)\n`;
            if (q.options.length > 0) {
                textPrompt += '选项:\n';
                q.options.forEach((o, j) => textPrompt += `  ${String.fromCharCode(65 + j)}. ${o.text}\n`);
            }
            if (q.blankCount) textPrompt += `(共${q.blankCount}个空)\n`;
            if (q.subQuestions) {
                textPrompt += '子题:\n';
                q.subQuestions.forEach((sq, si) => {
                    textPrompt += `  子题${si+1} [类型:${sq.type}]: ${sq.text}\n`;
                    sq.options?.forEach((o, j) => textPrompt += `    ${String.fromCharCode(65 + j)}. ${o.text}\n`);
                    if (sq.blankCount) textPrompt += `    (共${sq.blankCount}个空)\n`;
                });
            }
        });
        return { hasImages: false, text: textPrompt };
    }

    function callAI(promptData) {
        const cfg = loadConfig();
        const content = promptData.hasImages ? promptData.contentParts : promptData.text;
        return new Promise((resolve, reject) => {
            let url, headers, body;
            if (cfg.apiFormat === 'anthropic') {
                url = cfg.apiUrl.replace(/\/$/, '') + '/v1/messages';
                headers = { 'Content-Type': 'application/json', 'x-api-key': cfg.apiKey, 'anthropic-version': '2023-06-01' };
                body = JSON.stringify({ model: cfg.model, max_tokens: 8192, messages: [{ role: 'user', content }] });
            } else {
                url = cfg.apiUrl.replace(/\/$/, '');
                if (!url.endsWith('/chat/completions')) url += '/v1/chat/completions';
                headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}` };
                body = JSON.stringify({ model: cfg.model, messages: [{ role: 'user', content }], max_tokens: 8192, temperature: 0.1 });
            }
            GM_xmlhttpRequest({
                method: 'POST', url, headers, data: body,
                timeout: cfg.apiTimeout || 120000,
                onload(resp) {
                    try {
                        if (resp.status !== 200) {
                            reject(new Error(`HTTP ${resp.status}: ${resp.responseText?.substring(0, 300)}`));
                            return;
                        }
                        const data = JSON.parse(resp.responseText);
                        let text = cfg.apiFormat === 'anthropic'
                            ? (data.content?.[0]?.text || '')
                            : (data.choices?.[0]?.message?.content || '');
                        console.log('[AI] 原始返回:', text.substring(0, 500));
                        if (!text) { reject(new Error('AI返回空内容, resp: ' + resp.responseText?.substring(0, 300))); return; }
                        let parsed = null;
                        try { parsed = JSON.parse(text); } catch {}
                        if (!parsed) { const m = text.match(/```(?:json)?\s*([\s\S]*?)```/); if (m) try { parsed = JSON.parse(m[1].trim()); } catch {} }
                        if (!parsed) { const m = text.match(/\[[\s\S]*\]/); if (m) try { parsed = JSON.parse(m[0]); } catch {} }
                        if (parsed && Array.isArray(parsed)) resolve(parsed);
                        else reject(new Error('AI返回格式异常: ' + text.substring(0, 300)));
                    } catch (e) { reject(new Error('解析失败: ' + e.message + ' | resp: ' + resp.responseText?.substring(0, 200))); }
                },
                onerror(err) { reject(new Error('网络错误: ' + JSON.stringify(err).substring(0, 200))); },
                ontimeout() { reject(new Error('API请求超时 (' + Math.round((cfg.apiTimeout||120000)/1000) + 's)')); },
            });
        });
    }

    async function batchCallAI(questions, batchSize) {
        const allAnswers = [];
        const totalBatches = Math.ceil(questions.length / batchSize);
        for (let i = 0; i < questions.length; i += batchSize) {
            const batchIndex = Math.floor(i / batchSize) + 1;
            const batch = questions.slice(i, i + batchSize);
            log(`AI请求 第${batchIndex}/${totalBatches}批 (${batch.length}题)...`);
            updateProgress(batchIndex, totalBatches, 'AI请求');
            try {
                const answers = await callAI(buildPrompt(batch));
                allAnswers.push(...answers);
                log(`第${batchIndex}批 返回 ${answers.length} 个答案`, 'success');
            } catch (err) { log(`第${batchIndex}批 失败: ${err.message}`, 'error'); }
        }
        return allAnswers;
    }

    // ========== Step 4: 填入答案 ==========
    function waitForElement(selector, timeout = 5000) {
        return new Promise((resolve, reject) => {
            const el = document.querySelector(selector);
            if (el) return resolve(el);
            const observer = new MutationObserver(() => {
                const el = document.querySelector(selector);
                if (el) { observer.disconnect(); resolve(el); }
            });
            observer.observe(document.getElementById('c-grid-ajax') || document.body, { childList: true, subtree: true });
            setTimeout(() => { observer.disconnect(); reject(new Error('超时')); }, timeout);
        });
    }

    // ========== 代码答案浮动面板 ==========
    let codePanel = null;
    function getCodePanel() {
        if (codePanel) return codePanel;
        codePanel = document.createElement('div');
        codePanel.id = 'ai-code-panel';
        Object.assign(codePanel.style, {
            position: 'fixed', top: '50px', left: '10px', width: '420px',
            maxHeight: 'calc(100vh - 70px)', overflowY: 'auto', background: '#fff',
            borderRadius: '10px', zIndex: '99998', fontFamily: 'sans-serif',
            boxShadow: '0 4px 24px rgba(0,0,0,0.25)', border: '1px solid #ddd',
        });
        codePanel.innerHTML = `
            <div id="ai-code-header" style="background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:8px 14px;border-radius:10px 10px 0 0;display:flex;justify-content:space-between;align-items:center;cursor:move;">
                <span style="font-weight:bold;font-size:14px;">代码/简答题 参考答案</span>
                <button id="ai-code-toggle" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;line-height:1;">−</button>
            </div>
            <div id="ai-code-body" style="padding:0;"></div>`;
        document.body.appendChild(codePanel);
        // 折叠/展开
        codePanel.querySelector('#ai-code-toggle').addEventListener('click', function () {
            const body = document.getElementById('ai-code-body');
            if (body.style.display === 'none') { body.style.display = ''; this.textContent = '−'; }
            else { body.style.display = 'none'; this.textContent = '+'; }
        });
        // 拖拽
        let dragging = false, dx = 0, dy = 0;
        const header = codePanel.querySelector('#ai-code-header');
        header.addEventListener('mousedown', e => { dragging = true; dx = e.clientX - codePanel.offsetLeft; dy = e.clientY - codePanel.offsetTop; });
        document.addEventListener('mousemove', e => { if (!dragging) return; codePanel.style.left = (e.clientX - dx) + 'px'; codePanel.style.top = (e.clientY - dy) + 'px'; });
        document.addEventListener('mouseup', () => { dragging = false; });
        return codePanel;
    }

    function addCodeAnswer(qIndex, qText, code) {
        const panel = getCodePanel();
        const body = panel.querySelector('#ai-code-body');
        const item = document.createElement('div');
        item.style.cssText = 'border-bottom:1px solid #eee;';
        const shortText = qText.length > 60 ? qText.substring(0, 60) + '...' : qText;
        item.innerHTML = `
            <div style="background:#f8f8f8;padding:6px 12px;font-size:12px;color:#666;display:flex;justify-content:space-between;align-items:center;cursor:pointer;" class="ai-code-item-header">
                <span>第${qIndex}题: ${escapeHtml(shortText)}</span>
                <button class="ai-copy-btn" style="background:#667eea;color:#fff;border:none;padding:2px 10px;border-radius:3px;cursor:pointer;font-size:11px;">复制</button>
            </div>
            <pre class="ai-code-content" style="margin:0;padding:8px 12px;background:#fff;white-space:pre-wrap;word-break:break-all;max-height:250px;overflow-y:auto;font-size:12px;font-family:Consolas,monospace;user-select:text!important;display:none;">${escapeHtml(code)}</pre>`;
        // 点击标题展开/折叠
        item.querySelector('.ai-code-item-header').addEventListener('click', function (e) {
            if (e.target.classList.contains('ai-copy-btn')) return;
            const pre = item.querySelector('.ai-code-content');
            pre.style.display = pre.style.display === 'none' ? '' : 'none';
        });
        // 复制按钮
        item.querySelector('.ai-copy-btn').addEventListener('click', function () {
            navigator.clipboard.writeText(code).then(() => { this.textContent = '已复制'; setTimeout(() => { this.textContent = '复制'; }, 1500); });
        });
        body.appendChild(item);
    }

    // 模糊匹配选项:支持字母索引 + 文本内容匹配
    function fuzzyMatchOption(inputs, ansStr, qType) {
        const upper = ansStr.trim().toUpperCase();
        // 1. 按字母索引匹配 (A/B/C/D)
        if (/^[A-Z]$/.test(upper)) {
            const idx = upper.charCodeAt(0) - 65;
            if (idx >= 0 && idx < inputs.length) {
                $(inputs[idx]).prop('checked', true).click();
                return true;
            }
        }
        // 2. 判断题特殊匹配
        if (qType === 'JUDGE') {
            const trueSet = ['对', 'TRUE', 'T', '正确', '是', 'YES', 'Y', '1', 'A'];
            const falseSet = ['错', 'FALSE', 'F', '错误', '否', 'NO', 'N', '0', 'B'];
            const isTrue = trueSet.includes(upper);
            const isFalse = falseSet.includes(upper);
            for (let i = 0; i < inputs.length; i++) {
                const val = ($(inputs[i]).val() || '').toUpperCase();
                if ((isTrue && trueSet.includes(val)) || (isFalse && falseSet.includes(val))) {
                    $(inputs[i]).prop('checked', true).click();
                    return true;
                }
            }
        }
        // 3. 文本内容模糊匹配
        const cleanAnswer = ansStr.trim().replace(/^[A-Z][.、\s]+/, '').trim();
        for (let i = 0; i < inputs.length; i++) {
            const label = $(inputs[i]).closest('label').length ? $(inputs[i]).closest('label') : $(inputs[i]).parent();
            const optText = label.text().trim().replace(/^[A-Z][.、\s]+/, '').trim();
            if (optText === cleanAnswer || optText.includes(cleanAnswer) || cleanAnswer.includes(optText)) {
                $(inputs[i]).prop('checked', true).click();
                return true;
            }
        }
        return false;
    }

    async function fillOneQuestion(qData, answer) {
        const qId = qData.id;
        const navLink = document.querySelector(`dd[data-val-id="${qId}"] a`);
        if (!navLink) { log(`题${qData.index}: 找不到导航`, 'error'); return false; }
        navLink.click();
        await sleep(500);
        try { await waitForElement(`.question-item[id="${qId}"]`); } catch { log(`题${qData.index}: 加载超时`, 'error'); return false; }
        await sleep(300);

        const questionItem = $(`.question-item[id="${qId}"]`);
        const qType = questionItem.attr('data-type');

        try {
            if (isCodeType(qType)) {
                const answerStr = typeof answer === 'string' ? answer : JSON.stringify(answer, null, 2);
                addCodeAnswer(qData.index, qData.text, answerStr);
                log(`题${qData.index} [${qType}] 答案已添加到面板`, 'success');
                return 'codebox';
            }
            if (qType === 'SINGLE_CHIOCE' || qType === 'JUDGE') {
                const inputs = questionItem.find('input.question-option-input');
                if (!fuzzyMatchOption(inputs, String(answer), qType)) {
                    log(`题${qData.index}: 未匹配到选项 "${answer}"`, 'warn');
                }
            } else if (qType === 'MULIT_CHIOCE') {
                const inputs = questionItem.find('input.question-multi-input');
                const ansArr = Array.isArray(answer) ? answer : [answer];
                ansArr.forEach(a => fuzzyMatchOption(inputs, String(a), qType));
            } else if (qType === 'FILL_BLANK' || qType === 'PROGRAM_FILL_BLANK') {
                const blanks = questionItem.find('.question-blank-input');
                const ansArr = Array.isArray(answer) ? answer : [answer];
                blanks.each(function (idx) {
                    if (idx < ansArr.length) {
                        $(this).val(ansArr[idx]);
                        // 触发原生事件让页面感知到值变化
                        this.dispatchEvent(new Event('input', { bubbles: true }));
                        this.dispatchEvent(new Event('change', { bubbles: true }));
                        this.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, keyCode: 13 }));
                    }
                });
            } else if (qType === 'COMPLEX') {
                const ansArr = Array.isArray(answer) ? answer : [answer];
                questionItem.find('.sub-question').each(function (si) {
                    const subType = $(this).attr('data-type');
                    const subAns = ansArr[si];
                    if (subAns === undefined) return;
                    if (isCodeType(subType)) { addCodeAnswer(qData.index + '-' + (si+1), qData.text, typeof subAns === 'string' ? subAns : JSON.stringify(subAns, null, 2)); return; }
                    if (subType === 'SINGLE_CHIOCE' || subType === 'JUDGE') {
                        fuzzyMatchOption($(this).find('input.question-option-input, input.sub-question-option-input'), String(subAns), subType);
                    } else if (subType === 'MULIT_CHIOCE') {
                        (Array.isArray(subAns) ? subAns : [subAns]).forEach(a => fuzzyMatchOption($(this).find('input.question-multi-input'), String(a), subType));
                    } else if (subType === 'FILL_BLANK') {
                        const arr = Array.isArray(subAns) ? subAns : [subAns];
                        $(this).find('.sub-question-blank-input').each(function (idx) {
                            if (idx < arr.length) {
                                $(this).val(arr[idx]);
                                this.dispatchEvent(new Event('input', { bubbles: true }));
                                this.dispatchEvent(new Event('change', { bubbles: true }));
                                this.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, keyCode: 13 }));
                            }
                        });
                    }
                });
            }
            // 保存(代码题不自动保存)
            if (!isCodeType(qType)) {
                await sleep(300);
                if (typeof pageWindow.saveQuestionOne === 'function') {
                    pageWindow.saveQuestionOne(qId, () => { $(`dd[data-val-id="${qId}"]`).addClass('finish').removeClass('undo'); });
                }
            }
            log(`题${qData.index} [${qType}] 填入完成`, 'success');
            return true;
        } catch (err) { log(`题${qData.index} 填入出错: ${err.message}`, 'error'); return false; }
    }

    // ========== Step 5: 答案映射 & 填入 ==========
    async function fillAllAnswers(questions, answers) {
        const answerMap = {};
        answers.forEach(a => { if (a.id) answerMap[String(a.id)] = a.answer; });
        console.log('[AI] 答案映射:', Object.keys(answerMap));
        console.log('[AI] 题目IDs:', questions.map(q => q.id));

        const hasIdMatch = questions.some(q => answerMap[q.id] !== undefined);
        if (!hasIdMatch && answers.length > 0) log('ID匹配失败,改用顺序匹配', 'warn');

        const total = questions.length;
        const failed = [];
        let ansIdx = 0;
        for (let i = 0; i < total; i++) {
            const q = questions[i];
            updateProgress(i + 1, total, '填答');
            let answer = answerMap[q.id];
            if (answer === undefined && !hasIdMatch && ansIdx < answers.length) { answer = answers[ansIdx].answer; ansIdx++; }
            if (answer === undefined) { log(`题${q.index}: 无答案,跳过`, 'warn'); failed.push(q); continue; }
            const result = await fillOneQuestion(q, answer);
            if (!result) failed.push(q);
            await sleep(800);
        }
        if (failed.length > 0) log(`完成!${failed.length}题失败: ${failed.map(q => '第'+q.index+'题').join(', ')}`, 'warn');
        else log('全部完成!', 'success');
    }

    // ========== 主流程 ==========
    async function startAutoAnswer() {
        const cfg = loadConfig();
        if (!cfg.apiUrl || !cfg.apiKey) { alert('请先配置 API URL 和 API Key!'); showConfigPanel(); return; }
        log('开始自动答题...');
        try {
            unlockPage();
            const allQuestions = await fetchAllQuestions();
            if (allQuestions.length === 0) { log('没有抓到题目', 'error'); return; }

            // 按类型分组
            const choiceQs = allQuestions.filter(q => !isCodeType(q.type));
            const codeQs = allQuestions.filter(q => isCodeType(q.type));
            log(`分类: ${choiceQs.length}道选择/填空, ${codeQs.length}道代码/简答`);

            let allAnswers = [];

            // 选择/填空题
            if (choiceQs.length > 0) {
                log(`开始处理选择/填空题 (每批${cfg.batchSizeChoice}题)...`);
                const choiceAnswers = await batchCallAI(choiceQs, cfg.batchSizeChoice);
                allAnswers.push(...choiceAnswers);
                log(`选择/填空题 AI返回 ${choiceAnswers.length} 个答案`);
            }

            // 代码/简答题
            if (codeQs.length > 0 && cfg.enableCodeQuestions) {
                log(`开始处理代码/简答题 (每批${cfg.batchSizeCode}题)...`);
                const codeAnswers = await batchCallAI(codeQs, cfg.batchSizeCode);
                allAnswers.push(...codeAnswers);
                log(`代码题 AI返回 ${codeAnswers.length} 个答案`);
            } else if (codeQs.length > 0) {
                log(`跳过 ${codeQs.length} 道代码题 (未启用)`, 'warn');
            }

            log(`AI共返回 ${allAnswers.length} 个答案`);
            await fillAllAnswers(allQuestions, allAnswers);
        } catch (err) { log(`流程异常: ${err.message}`, 'error'); }
    }

    // ========== 注册菜单 & 按钮 ==========
    GM_registerMenuCommand('AI答题配置', showConfigPanel);
    GM_registerMenuCommand('开始自动答题', startAutoAnswer);
    GM_registerMenuCommand('解除页面限制', unlockPage);

    function addStartButton() {
        const bar = document.createElement('div');
        bar.id = 'ai-toolbar';
        Object.assign(bar.style, {
            position: 'fixed', top: '10px', left: '50%', transform: 'translateX(-50%)', zIndex: '99999',
            display: 'flex', gap: '8px', alignItems: 'center',
        });
        const btnStyle = 'padding:8px 16px;border:none;border-radius:20px;cursor:pointer;font-size:13px;color:#fff;font-weight:bold;';

        bar.innerHTML = `
            <button id="ai-btn-start" style="${btnStyle}background:linear-gradient(135deg,#667eea,#764ba2);box-shadow:0 4px 15px rgba(102,126,234,0.4);">AI 自动答题</button>
            <button id="ai-btn-unlock" style="${btnStyle}background:#e67e22;">解除限制</button>
            <button id="ai-btn-config" style="${btnStyle}background:#555;">配置</button>`;
        document.body.appendChild(bar);

        bar.querySelector('#ai-btn-start').addEventListener('click', startAutoAnswer);
        bar.querySelector('#ai-btn-unlock').addEventListener('click', () => { unlockPage(); alert('页面限制已解除!可以自由复制、右键了。'); });
        bar.querySelector('#ai-btn-config').addEventListener('click', showConfigPanel);
    }

    if (document.readyState === 'complete') addStartButton();
    else window.addEventListener('load', addStartButton);
})();