// ==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 = `[${time}] ${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 = `
AI回复预览:
准备就绪。点击右上角➖可最小化。
`;
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("❌ 请先设置 API Key", "log-err");
log("🔍 提取题目...", "log-match");
const questionData = extractQuestionsWithIndex();
if (!questionData) return log("❌ 未找到题目", "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(`❌ 出错: ${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);
})();