小牛椰椰🐄搜题助手

发布于 2 个月前

用于一些考试(救命sos)
将以下代码导入Tampermonkey开启即可使用
推荐自己上传本地题库(docx或者excel),也可以使用GPT(需自己填入API key)

"use strict";
/* eslint-disable no-underscore-dangle, @typescript-eslint/no-empty-function */
// ==UserScript==
// @name         🐄小牛🌴🥥搜题助手
// @namespace    search-answer
// @version      3.0
// @description  在线答题搜题脚本
// @author       Xiaoniuyeye
// @include      *
// @run-at       document-start
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require      https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.slim.min.js
// @require      https://greasyfork.org/scripts/418102-tm-request/code/TM_request.js?version=902218
// @require      https://cdn.jsdelivr.net/npm/mammoth@1.4.21/mammoth.browser.min.js
// @require      https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js
// @require      https://cdn.jsdelivr.net/npm/tinykeys@1.4.0/dist/tinykeys.umd.min.js
// @require      https://cdn.jsdelivr.net/npm/tesseract.js@2.1.5/dist/tesseract.min.js
// @require      https://cdn.jsdelivr.net/npm/js-md5@0.7.3/build/md5.min.js
// @require      https://cdn.jsdelivr.net/npm/fuse.js@6.4.6/dist/fuse.min.js
// @license      Apache-2.0
// @connect      api.openai.com
// @connect      www.baidu.com
// @connect      cn.bing.com
// @connect      www.google.com
// @connect      xiaoai.plus
// ==/UserScript==
(async () => {
    window.onblur = () => {
    };
    window.onfocus = () => {
    };
    document.onfocusin = () => {
    };
    document.onfocusout = () => {
    };
    document._addEventListener = document.addEventListener;
    document.addEventListener = (...argv) => {
        if (['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange'].includes(argv[0])) {
            return;
        }
        document._addEventListener(...argv);
    };
    document._removeEventListener = document.removeEventListener;
    document.removeEventListener = (...argv) => {
        if (['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange'].includes(argv[0])) {
            return;
        }
        document._removeEventListener(...argv);
    };
    window.onload = () => {
        window.onblur = () => {
        };
        window.onfocus = () => {
        };
        document.onfocusin = () => {
        };
        document.onfocusout = () => {
        };
    };
    let {highLightAbswer, startShortcutKey, ocrShortcutKey} = GM_getValue('settings') || {};
    const start = async () => {
        let data;

        let shortAnswer = "";
        let allShortAnswers = []; // 用于存储所有匹配结果
        let compatibility = "";
        let engine = 'baidu';
        const searchFromWebPage = (text, engine) => {
            switch (engine) {
                case 'baidu':
                    window.open(`https://www.baidu.com/s?wd=${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
                case 'bing':
                    window.open(`https://cn.bing.com/search?q=${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
                case 'google':
                    window.open(`https://www.google.com/search?q=111${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
                default:
                    window.open(`https://www.baidu.com/s?wd=${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
            }
            return null;
        };
        //引入OpenAI API或者中转API
        const openaiApiKey = '';// OpenAI API密钥

        const fetchAnswerFromOpenAI = async (text) => {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://xiaoai.plus/v1/chat/completions',//中转接口
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${openaiApiKey}`
                    },
                    data: JSON.stringify({
                        model: 'gpt-3.5-turbo',
                        messages: [
                            {role: "user", content: "回答我以下问题,给出正确的选项" + text}
                        ],
                        max_tokens: 300
                    }),
                    onload: (response) => {
                        try {
                            if (response.status === 200) {
                                const data = JSON.parse(response.responseText);
                                resolve(data.choices[0].message.content.trim()); // 使用正确的路径
                            } else {
                                console.error(`OpenAI API Error: ${response.status} ${response.statusText}`);
                                console.error(`Response Body: ${response.responseText}`); // 打印响应体
                                reject(new Error(`OpenAI API Error: ${response.statusText}`));
                            }
                        } catch (error) {
                            console.error(`Error parsing response: ${error.message}`);
                            reject(new Error(`Error parsing response: ${error.message}`));
                        }
                    },
                    onerror: (error) => {
                        console.error(`OpenAI API Network Error: ${error.statusText}`);
                        reject(new Error(`OpenAI API Network Error: ${error.statusText}`));
                    }
                });
            });
        };

        //计算匹配度
        const calPercentMatch = (text,titleContent) =>{
            let calResult = (text.length / (titleContent.replace(/<[^>]*>/g, '').length + 1)) * 100;
            //符号处理问题,可能会稍微大于100
            if(calResult > 100){
                calResult = 100
            }
            return " 匹配度:" + calResult.toFixed(2) + "%";
        }
        //文本处理
        const normalizeText = (text) => {
            return text
                .replace(/[\u3000\s]+/g, '') // 去除全角和半角空格
                .replace(/,/g, ',')         // 替换中文逗号为英文逗号
                .replace(/。/g, '.')         // 替换中文句号为英文句号
                .replace(/:/g, ':')         // 替换中文冒号为英文冒号
                .replace(/!/g, '!')         // 替换中文感叹号为英文感叹号
                .replace(/?/g, '?')         // 替换中文问号为英文问号
                .replace(/[({【]/g, '(')    // 转换为英文括号
                .replace(/[)}】]/g, ')');    // 转换为英文括号
        };
        const escapeRegExp = (text) => {
            return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        };
        //搜索
        const search = async (text) => {
            text = normalizeText(text);
            text = escapeRegExp(text);
            data = normalizeText(data);
            if (data === 'none') {
                return searchFromWebPage(text, engine);
            }

            const result = [];
            const regText = new RegExp(`${text}`, 'g'); // 匹配搜索文本
            const answerRegex = /答案[::]\s*[A-Za-z0-9\u4e00-\u9fa5]+/;

            //每一次调用search将简答结果重置
            allShortAnswers = [];

            let match;
            let lastIndex = 0; // 用来手动控制正则指针位置

            while ((match = regText.exec(data)) !== null) {
                if (match.index < lastIndex) {
                    console.warn("检测到可能的正则死循环,退出搜索");
                    break;
                }
                lastIndex = regText.lastIndex;

                const startIndex = match.index;
                // let endIndex = data.indexOf("\n", startIndex) !== -1 ? data.indexOf("\n", startIndex) : data.length;
                let endIndex = startIndex;
                //一直到答案位置
                const answerIndex = data.slice(startIndex).search(/答案/);
                //记录题干,有时候不是题干就会是一段内容。
                const titleIndex = data.slice(startIndex).search(/[\??。\.]/);
                if (answerIndex !== -1) {
                    //范围足够长,确保包含所有选项(多选情况下)
                    endIndex = startIndex + answerIndex + 20;
                } else {
                    // 如果没有找到“答案”,则默认截取一定范围
                    endIndex = startIndex + 800; // 增加范围以确保包括完整内容
                }
                // 当前题目的内容范围(包括答案)
                const allContent = data.slice(startIndex, endIndex);
                // 题目(不包括答案)
                const questionContent = data.slice(startIndex,endIndex - 20);
                const titleContent = data.slice(startIndex,startIndex + titleIndex);
                //计算匹配度
                compatibility = calPercentMatch(text,titleContent);
                console.log(
                    "题目范围:" + titleContent.replace(/<[^>]*>/g, '') +
                     calPercentMatch(text,titleContent)
                );
                // 提取答案
                const answerMatch = allContent.match(answerRegex);
                // shortAnswer = answerMatch ? answerMatch[0] : "未找到答案,可能是一段介绍"; // 更新全局变量
                shortAnswer = answerMatch && answerMatch.length > 0 ? answerMatch.join(", ") : "未找到答案,可能是一段介绍";
                // 存储结果
                result.push({
                    question: questionContent,
                    answer: shortAnswer,
                });
                allShortAnswers.push({
                    compatibility:compatibility,
                    answer: shortAnswer
                });
            }
            if (result.length === 0) {
                result.push({
                    question: text,
                    answer: "题库中没有答案!你可以框选完整题目采用GPT搜索"
                });
            }

            return result.map(item => `<p>${item.question}</p><p style="color:#ff001e">${item.answer}</p>`).join('<hr data-content="分隔线">');
        };




        //读取题库数据
        const readData = async () => {
            try {
                const data = await new Promise((res) => {
                    const input = $('<input type="file" id="search-answer-js" style="width:72px;height:25px;color:red;position:fixed;left:25%;top:25%;background-color:red;z-index:99999999" title="点此加载题库" multiple="multiple">');
                    $('body').append(input);
                    input[0].addEventListener('change', async function selectedFileChanged() {
                        if (this.files?.length) {
                            Swal.fire('读取&处理中...', 'Excel格式文件和题目较多时处理较慢,请耐心等待!');
                            Swal.showLoading();
                            await new Promise((resolve) => {
                                setTimeout(() => {
                                    resolve(true);
                                }, 1000);
                            });
                            const text = (await Promise.all([...(this.files || [])].map((file) => new Promise((resolve) => {
                                const reader = new FileReader();
                                const fileName = file.name;
                                reader.onabort = () => resolve('');
                                reader.onerror = () => resolve('');
                                if (/.*?\.docx?$/.test(fileName)) {
                                    reader.onload = async () => {
                                        const arrayBuffer = reader.result;
                                        const options = {
                                            convertImage: mammoth.images.imgElement((image) => image.read('base64').then((imageBuffer) => {
                                                const imageMd5 = md5(imageBuffer);
                                                imagesData[imageMd5] = `data:${image.contentType};base64,${imageBuffer}`;
                                                return {
                                                    src: `$${imageMd5}$`
                                                };
                                            }))
                                        };
                                        const {value: fileData} = await mammoth.convertToHtml({arrayBuffer}, options);
                                        resolve(fileData);
                                    };
                                    reader.readAsArrayBuffer(file);
                                } else if (/.*?\.xlsx?$/.test(fileName)) {
                                    reader.onload = async () => {
                                        const arrayBuffer = reader.result;
                                        const {Sheets} = XLSX.read(arrayBuffer);
                                        // eslint-disable-next-line max-len
                                        const fileData = Object.values(Sheets).map((sheet) => XLSX.utils.sheet_to_json(sheet, {header: 1}).map((cell) => cell.map((value) => value?.toString()?.trim()).filter((value) => value)
                                            .join(' | '))
                                            .join('<br/>'))
                                            .join('<br/>');
                                        resolve(fileData);
                                    };
                                    reader.readAsArrayBuffer(file);
                                } else {
                                    reader.onload = () => {
                                        const fileData = reader.result;
                                        if (!fileData) {
                                            return resolve('');
                                        }
                                        resolve(fileData);
                                    };
                                    reader.readAsText(file);
                                }
                            })))).join('<br/>');
                            GM_setValue('data0', text);
                            Swal.fire('题库加载完毕!');
                            input.remove();
                            res(text);
                        }
                    });
                    document.querySelector('#search-answer-js').click();
                });
                return {text: data};
            } catch (error) {
                console.error(error);
                Swal.fire('题库加载失败!', '详情请查看控制台', 'error');
                return {};
            }
        };
        await Swal.fire({
            title: '是否加载题库?',
            html: '加载题库:如果你有题库,请加载你的题库(推荐)<br/>' +
                  '直接运行:如之前加载过题库,并且不需要重新加载题库<br/>' +
                  '无题库模式:弹出网页显示搜索结果',
            confirmButtonText: '加载题库',
            showCancelButton: true,
            cancelButtonText: '直接运行',
            showDenyButton: true,
            denyButtonText: '无题库模式'
        }).then(async ({isConfirmed, isDenied}) => {
            if (isConfirmed) {
                data = (await readData()).text;
            } else if (isDenied) {
                data = 'none';
                const {value: selectedEngine} = await Swal.fire({
                    title: '请选择搜索引擎',
                    input: 'radio',
                    inputOptions: {
                        baidu: '百度',
                        bing: '必应',
                        google: '谷歌'
                    },
                    inputValidator: (value) => {
                        if (!value) {
                            return '请选择一个搜索引擎!';
                        }
                        return '';
                    }
                });
                if (selectedEngine) {
                    engine = selectedEngine;
                }
            } else {
                data = GM_getValue('data0');
            }
        });
        if (!data)
            return Swal.fire('加载题库失败', '', 'error');

        const createButton = (name) => {
            const button = document.createElement('button');
            button.setAttribute('style', '' +
                'width:38px!important;' +
                'height:24px!important;' +
                'background:#fff!important;' +
                'display:none!important;' +
                'border:0px!important;' +
                'border-radius:5px!important;' +
                'box-shadow:4px 4px 8px #888!important;' +
                'position:absolute!important;' +
                'z-index:999999999!important;' +
                'font-size: 12px;text-align-last: center;' +
                'cursor: pointer;' +
                '');
            button.textContent = name;
            button.style.display = 'none';
            button.style.zIndex = '9999999999';
            return document.body.appendChild(button);
        };
        //创建按钮
        const searchButton = createButton('搜索');
        const gptButton = createButton("GPT");
        let showButton = false;
        if(searchButton.style.display === 'none' && gptButton.style.display === 'none'){
            showButton = true;
        }
        const showResultInPage = (result, selection) => {
            if (result) {
                // 定义唯一标识符,用于标记动态生成的 div
                const resultDivClass = 'custom-result-div';

                // 查找页面上所有旧的结果 div,并清除
                const existingDivs = document.querySelectorAll(`.${resultDivClass}`);
                existingDivs.forEach(div => div.remove());

                // 获取选中内容的父元素
                const parentElement = selection.anchorNode.parentElement;

                // 创建新的 div,包含 p 标签和按钮
                const newDiv = document.createElement('div');
                newDiv.classList.add(resultDivClass); // 给 div 添加唯一 class
                const divStyles = {
                    display: 'flow-root',
                    border: 'none',
                    backgroundColor: '#fafafa',
                    fontSize: '14px',
                    lineHeight: '1.6'
                };
                Object.assign(newDiv.style, divStyles);

                // 创建 p 标签并设置内容
                const pElement = document.createElement('p');
                pElement.innerText = allShortAnswers.length > 0
                    ? allShortAnswers.map(item => `${item.compatibility}, ${item.answer}`).join('\n')
                    : "未找到匹配内容,可以适当缩小题干范围或全选采用GPT搜索";

                pElement.style.margin = '0'; // 去掉 p 标签默认的上下边距
                pElement.style.color = '#ef0606'; // 统一文本颜色
                newDiv.appendChild(pElement);

                // 创建 button 按钮
                const confirmButton = document.createElement('button');
                confirmButton.innerText = '详解';
                confirmButton.type = 'button'; // 避免默认 submit 刷新页面
                const buttonStyles = {
                    marginTop: '10px',
                    padding: '6px 12px',
                    backgroundColor: 'rgb(255,255,255)',
                    color: 'rgba(0,0,0,0.75)',
                    border: '1px solid rgba(30,31,34,0.31)',
                    borderRadius: '5px',
                    cursor: 'pointer',
                    fontSize: '14px',
                    transition: 'background-color 0.3s ease'
                };
                Object.assign(confirmButton.style, buttonStyles);

                // 按钮悬停时改变颜色
                confirmButton.onmouseenter = () => {
                    confirmButton.style.backgroundColor = 'rgba(0,0,0,0.75)';
                    confirmButton.style.color = 'rgb(255,255,255)';
                };
                confirmButton.onmouseleave = () => {
                    confirmButton.style.backgroundColor = '#ffffff';
                    confirmButton.style.color = 'rgba(0,0,0,0.75)';
                };

                // 按钮点击事件
                confirmButton.addEventListener('click', () => {
                    Swal.fire({
                        title: '搜索结果',
                        html: result,
                        width: '20vm',
                        heightAuto: true,
                        allowOutsideClick: true,
                        confirmButtonText: '关闭'
                    });
                });

                newDiv.appendChild(confirmButton);

                // 将新的 div 添加到选中内容所在标签下方
                parentElement.insertAdjacentElement('afterend', newDiv); // 插入到父元素之后

                // 取消选中
                selection.removeAllRanges();
            } else {
                alert("请先选择一些内容!");
            }
        };


        document.onmouseup = async function (e) {
            // 获取选中的文本
            const selection = window.getSelection();
            let selectedText = selection?.toString()?.trim();

            selectedText = escapeRegExp(selectedText);

            // 确保选中有内容
            if (selectedText.length > 0 && showButton) {
                // 显示按钮
                searchButton.style.display = 'block';
                searchButton.style.top = `${e.pageY + 10}px`;
                searchButton.style.left = `${e.pageX + 10}px`;
                gptButton.style.display = 'block';
                gptButton.style.top = `${e.pageY + 35}px`;
                gptButton.style.left = `${e.pageX + 10}px`;

                // 绑定搜索按钮点击事件
                searchButton.onclick = async function () {
                    searchButton.style.display = 'none';
                    gptButton.style.display = 'none';
                    const result = await search(selectedText);
                    //显示详情在原网页中
                    showResultInPage(result,selection);
                };
                //绑定GPT搜索点击事件
                gptButton.onclick = async function () {
                    searchButton.style.display = 'none';
                    gptButton.style.display = 'none';

                    // 显示加载中的提示
                    const loadingSwal = Swal.fire({
                        title: '请稍候一下',
                        html: '正在等待ChatGPT回复...',
                        allowOutsideClick: false, // 禁止在加载中关闭对话框
                        didOpen: () => {
                            Swal.showLoading(); // 显示加载动画
                        }
                    });
                    try {
                        // 异步获取 OpenAI 结果
                        const openaiResult = await fetchAnswerFromOpenAI(selectedText);

                        // 关闭加载框并显示结果
                        Swal.fire({
                            title: 'GPT结果',
                            html: openaiResult ? `<p>${openaiResult}</p>` : '未找到答案',
                            width: '20vm',
                            heightAuto: true,
                            allowOutsideClick: true,
                            confirmButtonText: '关闭'
                        });
                    } catch (error) {
                        // 如果发生错误,显示错误信息
                        Swal.fire({
                            icon: 'error',
                            title: '出错了',
                            text: '请求处理失败,请稍后重试',
                            confirmButtonText: '关闭'
                        });
                    }
                };

            } else {
                searchButton.style.display = 'none';
                gptButton.style.display = 'none';
            }
        };



    };
    const tinykeysOptions = {};
    if (startShortcutKey) {
        tinykeysOptions[startShortcutKey] = start;
    }
    window.tinykeys.default(window, tinykeysOptions);
    GM_registerMenuCommand('启动', start);
    GM_addStyle(`
        .swal2-container {
          z-index: 9999999999 !important;
        }
        .swal2-html-container *{
          left:0;
          padding-left:0 !important;
          margin-left:0;
          border-left:0;
          width:100%;
        }
        .swal2-html-container hr{
          color: #a2a9b6;
          border: 0;
          font-size: 12px;
          padding: 1em 0;
          position: relative;
        }
        .swal2-html-container hr::before {
          content: attr(data-content);
          position: absolute;
          padding: 0 1ch;
          line-height: 1px;
          border: solid #d0d0d5;
          border-width: 0 99vw;
          width: fit-content;
          white-space: nowrap;
          left: 50%;
          transform: translateX(-50%);
        }
        .swal2-html-container hr::after{
          content: attr(data-content);
          position: absolute;
          padding: 4px 1ch;
          top: 50%; left: 50%;
          transform: translate(-50%, -50%);
          color: transparent;
          border: 1px solid #d0d0d5;
        }
        .swal2-html-container .setting {
          text-align: left;
        }
        .swal2-html-container input[type="checkbox"]{
          width: 15px;
        }
        .swal2-html-container input[type="text"]{
          width: 200px;
          border: 2px solid #00a9fd;
          border-radius: 5px;
          font-size: 15px;
        }
    `);
})();

Tampermonkey脚本
$ cd ..