// ==UserScript==
// @name 厦门理工高校邦考试AI助手 (DeepSeek)
// @namespace https://github.com/Wu557666/gaoxiaobangai
// @version 1.1.3
// @description 在厦门理工高校邦考试/测验页面调用 DeepSeek API 自动答题,支持自定义 API 地址
// @author Wu557666
// @icon https://favicon.im/xmut.gaoxiaobang.com?size=128
// @match https://xmut.class.gaoxiaobang.com/class/*/exam/*
// @match https://xmut.class.gaoxiaobang.com/class/*/quiz/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @connect api.deepseek.com
// @connect api.deepseek.com
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// ========== 默认配置 ==========
const DEFAULT_API_URL = 'https://api.deepseek.com/chat/completions';
const DEFAULT_MODEL = 'deepseek-chat';
// ========== 存储键名 ==========
const STORAGE_API_KEY = 'deepseek_api_key';
const STORAGE_API_URL = 'deepseek_api_url';
const STORAGE_MODEL = 'deepseek_model';
// ========== 获取/设置配置 ==========
function getApiKey() {
return GM_getValue(STORAGE_API_KEY, '');
}
function setApiKey(key) {
GM_setValue(STORAGE_API_KEY, key);
}
function getApiUrl() {
return GM_getValue(STORAGE_API_URL, DEFAULT_API_URL);
}
function setApiUrl(url) {
GM_setValue(STORAGE_API_URL, url);
}
function getModel() {
return GM_getValue(STORAGE_MODEL, DEFAULT_MODEL);
}
function setModel(model) {
GM_setValue(STORAGE_MODEL, model);
}
// ========== 页面内弹窗配置 ==========
function showSettingsPanel() {
const oldPanel = document.getElementById('ai-settings-panel');
if (oldPanel) oldPanel.remove();
const apiKey = getApiKey();
const apiUrl = getApiUrl();
const model = getModel();
const overlay = document.createElement('div');
overlay.id = 'ai-settings-overlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:99999;display:flex;align-items:center;justify-content:center;';
const panel = document.createElement('div');
panel.id = 'ai-settings-panel';
panel.style.cssText = 'background:white;padding:24px;border-radius:16px;box-shadow:0 10px 40px rgba(0,0,0,0.3);width:400px;max-width:90vw;font-family:Arial,sans-serif;';
panel.innerHTML = `
🤖 考试 AI 助手设置
请输入你的 DeepSeek API Key(点击获取)
高级设置(可选)
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
document.getElementById('ai-settings-cancel').onclick = () => {
overlay.remove();
if (!getApiKey()) {
const tip = document.createElement('div');
tip.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9999;background:#ff9800;color:white;padding:10px 15px;border-radius:8px;';
tip.innerText = '⚠️ 未设置 API Key,无法使用';
document.body.appendChild(tip);
setTimeout(() => tip.remove(), 3000);
}
};
document.getElementById('ai-settings-save').onclick = () => {
const newKey = document.getElementById('ai-api-key-input').value.trim();
const newUrl = document.getElementById('ai-api-url-input').value.trim();
const newModel = document.getElementById('ai-model-input').value.trim();
if (!newKey) {
alert('❌ API Key 不能为空!');
return;
}
setApiKey(newKey);
if (newUrl) setApiUrl(newUrl);
if (newModel) setModel(newModel);
overlay.remove();
alert('✅ 配置已保存!现在可以使用 AI 答题了。');
location.reload();
};
}
// 注册菜单命令
GM_registerMenuCommand('⚙️ 打开设置面板', showSettingsPanel);
GM_registerMenuCommand('📋 查看当前配置', () => {
const key = getApiKey();
const url = getApiUrl();
const model = getModel();
const keyPreview = key ? key.slice(0, 8) + '...' + key.slice(-4) : '未设置';
alert(`当前配置:\n\nAPI Key: ${keyPreview}\nAPI 地址: ${url}\n模型: ${model}`);
});
// ========== 初始化检查 ==========
const apiKey = getApiKey();
if (!apiKey) {
console.warn('⚠️ 未设置 DeepSeek API Key,正在打开设置面板...');
setTimeout(showSettingsPanel, 1000);
return;
}
console.log('✅ DeepSeek 配置已加载');
// ========== 题目提取(适配高校邦实际结构) ==========
function extractQuestion() {
// 小测题目
const quizP = document.querySelector('.quiz-title p');
if (quizP) return quizP.innerText.trim();
// 考试题目备选
const selectors = [
'.exam-question', '.question-title', '.quiz-question',
'.question-content', '.subject-title', '.stem',
'.question-item .title', '.exam-item .title'
];
for (let sel of selectors) {
const el = document.querySelector(sel);
if (el) return el.innerText.trim();
}
return null;
}
// ========== 选项提取(精准适配 .answer-wap .answer) ==========
function extractOptions() {
const options = [];
const answerWap = document.querySelector('.answer-wap');
if (!answerWap) return options;
const answerItems = answerWap.querySelectorAll('.answer');
answerItems.forEach(item => {
// 提取字母 "A." 和内容
const letterSpan = Array.from(item.childNodes).find(node => node.nodeType === 3 && node.nodeValue.trim());
const letter = letterSpan ? letterSpan.nodeValue.trim() : '';
const content = item.querySelector('p')?.innerText.trim() || '';
const fullText = letter + ' ' + content;
if (fullText.trim()) options.push(fullText.trim());
});
return options;
}
// ========== 选项选择(适配 .answer 点击图标) ==========
function selectOption(answer) {
const letters = answer.match(/[A-D]/gi);
if (!letters) {
console.warn('⚠️ 未识别到有效答案字母:', answer);
return;
}
const answerWap = document.querySelector('.answer-wap');
if (!answerWap) return;
const answerItems = answerWap.querySelectorAll('.answer');
letters.forEach(letter => {
const targetIndex = letter.toUpperCase().charCodeAt(0) - 65;
const targetItem = answerItems[targetIndex];
if (!targetItem) return;
const icon = targetItem.querySelector('i');
if (!icon) return;
// 检查是否已选中(高校邦的选中类可能是 gxb-icon-radio-selected 等)
const isSelected = icon.classList.contains('gxb-icon-radio-selected') ||
icon.classList.contains('gxb-icon-check-selected') ||
icon.classList.contains('selected');
if (!isSelected) {
icon.click();
console.log(`📌 已选择: ${letter}`);
} else {
console.log(`⏭️ 选项 ${letter} 已选中,跳过`);
}
});
}
// ========== AI 答题核心 ==========
async function answerCurrentQuestion() {
const apiKeyNow = getApiKey();
if (!apiKeyNow) {
alert('请先设置 API Key!');
showSettingsPanel();
return;
}
const question = extractQuestion();
if (!question) {
alert('❌ 未检测到题目,请确保在考试/测验页面');
return;
}
const options = extractOptions();
if (!options.length) {
alert('❌ 未检测到选项');
return;
}
const optionsText = options.join('\n');
const prompt = `请回答以下题目,只返回正确答案的字母(如 A, B, C 或 AB):\n题目:${question}\n选项:\n${optionsText}`;
const apiUrl = getApiUrl();
const model = getModel();
console.log('🤖 正在调用 AI...');
console.log(' 题目:', question);
console.log(' 选项:', optionsText);
GM_xmlhttpRequest({
method: 'POST',
url: apiUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKeyNow}`
},
data: JSON.stringify({
model: model,
messages: [
{ role: 'system', content: '你是一个考试答题助手,只返回正确答案的字母,不要任何解释。' },
{ role: 'user', content: prompt }
],
temperature: 0.1
}),
onload: function(resp) {
try {
const data = JSON.parse(resp.responseText);
const answer = data.choices[0].message.content.trim();
console.log('✅ AI 答案:', answer);
selectOption(answer);
} catch (e) {
console.error('解析 AI 响应失败:', e);
alert('AI 响应解析失败,请查看控制台');
}
},
onerror: function(err) {
console.error('API 请求失败:', err);
alert('AI 调用失败,请检查网络或 API 地址是否正确。');
}
});
}
// ========== 浮动控制面板 ==========
function addControlPanel() {
const panel = document.createElement('div');
panel.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9998;background:#1fb6ff;color:white;padding:12px 18px;border-radius:12px;box-shadow:0 4px 15px rgba(0,0,0,0.2);display:flex;align-items:center;gap:12px;font-family:Arial,sans-serif;';
const status = document.createElement('span');
status.innerText = '🤖 AI 就绪';
status.style.fontWeight = 'bold';
const btn = document.createElement('button');
btn.innerText = '答题';
btn.style.cssText = 'background:white;color:#1fb6ff;border:none;padding:6px 16px;border-radius:6px;cursor:pointer;font-weight:bold;font-size:14px;';
btn.onclick = () => {
status.innerText = '🤖 AI 思考中...';
answerCurrentQuestion().finally(() => {
status.innerText = '🤖 AI 就绪';
});
};
const settingsBtn = document.createElement('button');
settingsBtn.innerText = '⚙️';
settingsBtn.style.cssText = 'background:transparent;color:white;border:none;font-size:18px;cursor:pointer;padding:0 4px;';
settingsBtn.title = '打开设置';
settingsBtn.onclick = showSettingsPanel;
panel.appendChild(status);
panel.appendChild(btn);
panel.appendChild(settingsBtn);
document.body.appendChild(panel);
}
setTimeout(addControlPanel, 1500);
})();