1558天 咸鱼也有梦想

重要的人越来越少,剩下的人也越来越重要 ​​

南京专技公需课答题助手

发布于 2个月前 / 81 次围观 / 0 条评论 / 资源分享 / 咸鱼

 

油猴脚本,需去火山引擎申请推理模型才可使用

https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint

// ==UserScript==
// @name         南京专技公需课助手 (V5.0)
// @namespace    匿名
// @version      5.0
// @description  进入答题页面,点击智能答题按钮,脚本自动答题。本脚本利用豆包AI实现,所以需要去火山引擎申请API Key和Model ID ,推荐申请Doubao-1.5-pro-32k|250115 模型使用。
// @author       匿名
// @match        *://*.mynj.cn*/*
// @connect      ark.cn-beijing.volces.com
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    const DEFAULT_API_URL = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";

    // --- 样式区域 ---
    GM_addStyle(`
        /* 主面板样式 */
        #ai-panel {
            position: fixed; top: 10px; right: 10px; width: 380px;
            background: #fff; border: 2px solid #007ee2; box-shadow: 0 0 20px rgba(0,0,0,0.3);
            z-index: 2147483647;
            padding: 12px; font-family: "Microsoft YaHei", sans-serif; border-radius: 8px;
            display: flex; flex-direction: column; max-height: 90vh;
            transition: opacity 0.3s;
        }

        /* 标题栏布局 */
        .panel-header {
            display: flex; justify-content: space-between; align-items: center;
            border-bottom: 1px solid #eee; padding-bottom: 8px; margin-bottom: 8px;
        }
        .panel-header h3 { margin: 0; font-size: 16px; color: #007ee2; font-weight: bold; }
        .panel-minimize-btn {
            cursor: pointer; font-size: 20px; line-height: 1; color: #666;
            padding: 0 5px; transition: color 0.2s;
        }
        .panel-minimize-btn:hover { color: #007ee2; font-weight: bold; }

        /* 悬浮球样式 (默认隐藏) */
        #ai-float-ball {
            position: fixed; top: 40%; right: 0;
            width: 40px; height: 40px;
            background: #007ee2; border-radius: 20px 0 0 20px;
            color: #fff; font-size: 20px; text-align: center; line-height: 40px;
            cursor: pointer; z-index: 2147483647;
            box-shadow: -2px 2px 10px rgba(0,0,0,0.2);
            display: none; /* 初始状态根据逻辑控制 */
            transition: width 0.2s;
        }
        #ai-float-ball:hover { width: 50px; background: #005bb5; }

        /* 内容区域 */
        #ai-response-area {
            width: 96%; height: 200px;
            border: 1px solid #ccc; border-radius: 4px;
            padding: 8px; font-size: 13px; line-height: 1.5; color: #333;
            overflow-y: auto; margin-bottom: 8px; background: #f8f9fa; font-family: Consolas, monospace;
        }

        #log-window {
            background: #2d2d2d; color: #eee;
            font-family: Consolas, monospace; font-size: 11px;
            padding: 8px; border-radius: 4px; margin-bottom: 8px;
            flex-grow: 1; overflow-y: auto; height: 150px;
            white-space: pre-wrap; word-break: break-all;
        }
        .log-match { color: #a6e22e; }
        .log-warn { color: #e6db74; }
        .log-err { color: #f92672; }

        #ai-panel button { width: 100%; padding: 10px; cursor: pointer; background: #007ee2; color: #fff; border: none; border-radius: 4px; margin-bottom: 5px; font-size:14px; font-weight:bold;}
        #ai-panel button:hover { background: #005bb5; }
        #ai-panel button.secondary { background: #e9ecef; color: #495057; font-weight:normal; border:1px solid #ced4da; font-size:12px; padding:6px;}

        #config-area { display: none; background: #f1f3f5; padding: 10px; border-radius:4px; margin-top: 5px;}
        #config-area input { width: 95%; padding: 6px; margin-bottom: 6px; border: 1px solid #adb5bd; border-radius:3px;}
    `);

    // --- 日志 ---
    function log(msg, type = '') {
        const logWin = document.getElementById('log-window');
        if (!logWin) return;
        const line = document.createElement('div');
        const time = new Date().toTimeString().split(' ')[0];
        line.innerHTML = `<span style="opacity:0.5">[${time}]</span> ${msg}`;
        if(type) line.className = type;
        logWin.appendChild(line);
        logWin.scrollTop = logWin.scrollHeight;
    }

    // --- UI 创建 ---
    function createUI() {
        if (document.getElementById('ai-panel')) return;

        // 1. 创建悬浮球
        const ball = document.createElement('div');
        ball.id = 'ai-float-ball';
        ball.innerHTML = '√ '; // 或者是 '答'
        ball.title = '点击展开答题助手';
        document.body.appendChild(ball);

        // 2. 创建主面板
        const panel = document.createElement('div');
        panel.id = 'ai-panel';
        panel.innerHTML = `
            <div class="panel-header">
                <h3>√  答题助手 V5.0</h3>
                <span id="btn-minimize" class="panel-minimize-btn" title="最小化">➖</span>
            </div>

            <div style="font-size:12px; color:#666; margin-bottom:5px;">AI回复预览:</div>
            <textarea id="ai-response-area" placeholder="AI答案将显示在这里..."></textarea>

            <div id="log-window">准备就绪。点击右上角  可最小化。</div>

            <button id="btn-auto-run">√  开始智能答题</button>
            <div style="display:flex; gap:5px;">
                 <button id="btn-toggle-config" class="secondary" style="flex:1;">API 设置</button>
                 <button id="btn-exec-text" class="secondary" style="flex:1;">√  仅执行文本</button>
            </div>

            <div id="config-area">
                <label>API Key:</label><input type="password" id="cfg-key">
                <label>Model ID:</label><input type="text" id="cfg-model">
                <button id="btn-save-config" class="secondary" style="background:#28a745;color:#fff;width:100%">保存配置</button>
            </div>
        `;
        document.body.appendChild(panel);

        // --- 事件绑定 ---

        // 最小化/展开逻辑
        const btnMin = document.getElementById('btn-minimize');

        // 点击最小化按钮
        btnMin.onclick = () => {
            panel.style.display = 'none';
            ball.style.display = 'block';
        };

        // 点击悬浮球
        ball.onclick = () => {
            ball.style.display = 'none';
            panel.style.display = 'flex';
        };

        // 功能按钮
        document.getElementById('btn-auto-run').onclick = runAutoProcess;
        document.getElementById('btn-exec-text').onclick = () => {
            const text = document.getElementById('ai-response-area').value;
            if(!text) return log("文本框为空!", "log-err");
            parseAndFillByPosition(text);
        };
        document.getElementById('btn-toggle-config').onclick = () => {
            const el = document.getElementById('config-area');
            el.style.display = el.style.display === 'block' ? 'none' : 'block';
        };
        document.getElementById('btn-save-config').onclick = saveConfig;

        loadConfigToUI();
    }

    function saveConfig() {
        GM_setValue('doubao_key', document.getElementById('cfg-key').value.trim());
        GM_setValue('doubao_model', document.getElementById('cfg-model').value.trim());
        log("配置已保存");
        document.getElementById('config-area').style.display = 'none';
    }
    function loadConfigToUI() {
        document.getElementById('cfg-key').value = GM_getValue('doubao_key', '');
        document.getElementById('cfg-model').value = GM_getValue('doubao_model', '');
    }

    // --- 主流程 (保持 V4.1 逻辑) ---
    async function runAutoProcess() {
        const apiKey = GM_getValue('doubao_key');
        const modelId = GM_getValue('doubao_model');
        if (!apiKey || !modelId) return log(" X   请先设置 API Key", "log-err");

        log("√ 提取题目...", "log-match");
        const questionData = extractQuestionsWithIndex();
        if (!questionData) return log(" X   未找到题目", "log-err");

        log(`√  提取成功,共 ${questionData.count} 题`, "log-match");

        log("√  请求豆包...", "log-match");
        try {
            const aiResponse = await callDoubaoAPI(apiKey, modelId, questionData.text);
            document.getElementById('ai-response-area').value = aiResponse;
            log("√ AI回复已接收", "log-match");
            parseAndFillByPosition(aiResponse);
        } catch (error) {
            log(` X  出错: ${error.message}`, "log-err");
        }
    }

    function extractQuestionsWithIndex() {
        let text = "";
        const panels = document.querySelectorAll('.exam-subject-text-panel');
        if(panels.length === 0) return null;

        panels.forEach((panel, index) => {
            let rawText = panel.innerText.replace(/\n\s*\n/g, '\n').trim();
            text += `【第 ${index + 1} 题】\n${rawText}\n\n`;
        });
        return { text: text, count: panels.length };
    }

    function callDoubaoAPI(apiKey, modelId, content) {
        return new Promise((resolve, reject) => {
            const systemPrompt = `你是一个答题机器人。
1. 请按题号输出答案,只输出字母。
2. 格式示例:
   1. A
   2. ABC
   3. 正确
3. 忽略选项文字,只回字母!多选题字母连写。`;

            GM_xmlhttpRequest({
                method: "POST",
                url: DEFAULT_API_URL,
                headers: { "Content-Type": "application/json", "Authorization": "Bearer " + apiKey },
                data: JSON.stringify({
                    "model": modelId,
                    "messages": [
                        { "role": "system", "content": systemPrompt },
                        { "role": "user", "content": content }
                    ],
                    "temperature": 0.1
                }),
                onload: function(response) {
                    if (response.status === 200) {
                        try {
                            const json = JSON.parse(response.responseText);
                            resolve(json.choices[0].message.content);
                        } catch (e) { reject(new Error("JSON解析失败")); }
                    } else { reject(new Error(`HTTP ${response.status}`)); }
                },
                onerror: function(err) { reject(new Error("网络错误")); }
            });
        });
    }

    function parseAndFillByPosition(input) {
        log("√ 开始执行...", "log-match");
        const lines = input.split('\n');
        const panels = document.querySelectorAll('.exam-subject-text-panel');
        let successCount = 0;

        lines.forEach(line => {
            line = line.trim();
            if (!line) return;
            const match = line.match(/^(\d+)[\.\s:、]*([A-Z]+|正确|错误)/);

            if (match) {
                const qIndex = parseInt(match[1]) - 1;
                const answer = match[2];

                if (qIndex >= 0 && qIndex < panels.length) {
                    const currentPanel = panels[qIndex];
                    if (answer === "正确" || answer === "错误") {
                        const val = (answer === "正确") ? "1" : "0";
                        const radio = currentPanel.querySelector(`input[value="${val}"]`);
                        if (radio) {
                            if (!radio.checked) radio.click();
                            log(`   [第${qIndex+1}题] -> ${answer}`, "log-match");
                            successCount++;
                        }
                    } else {
                        const inputs = currentPanel.querySelectorAll('input[type="radio"], input[type="checkbox"]');
                        if (inputs.length === 0) return;
                        const letters = answer.split('');
                        let clicked = false;
                        letters.forEach(char => {
                            const optIndex = char.charCodeAt(0) - 65;
                            if (inputs[optIndex]) {
                                if(!inputs[optIndex].checked) inputs[optIndex].click();
                                clicked = true;
                            }
                        });
                        if(clicked) {
                            log(`   [第${qIndex+1}题] -> ${answer}`, "log-match");
                            successCount++;
                        }
                    }
                }
            }
        });
        log(`√ 完毕! 勾选 ${successCount} 题。`, "log-match");
    }

    setTimeout(createUI, 1000);
})();