小牛椰椰🐄搜题助手
发布于 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;
}
`);
})();