// ==UserScript==
// @name 超星学习通 AI 智能答题助手
// @namespace https://github.com/chaoxing-ai-answer
// @version 2.0.0
// @description 超星学习通章节测验/考试 AI 自动答题,支持单选、多选、判断、填空题,可自定义 API Key 和模型
// @author AI Assistant
// @match *://mooc1.chaoxing.com/*
// @match *://mooc1-1.chaoxing.com/*
// @match *://mooc1-2.chaoxing.com/*
// @match *://mooc2.chaoxing.com/*
// @match *://i.chaoxing.com/*
// @match *://*.chaoxing.com/exam*
// @match *://*.chaoxing.com/work*
// @match *://*.chaoxing.com/mooc*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @connect api.openai.com
// @connect api.deepseek.com
// @connect dashscope.aliyuncs.com
// @connect open.bigmodel.cn
// @connect aip.baidubce.com
// @connect openrouter.ai
// @connect api.openrouter.ai
// @connect api.siliconflow.cn
// @connect api.moonshot.cn
// @connect api.lingyiwanwu.com
// @connect api.baichuan-ai.com
// @connect api.minimax.chat
// @connect api.groq.com
// @connect generativelanguage.googleapis.com
// @connect api.anthropic.com
// @connect localhost
// @connect 127.0.0.1
// @connect *
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ===================== 默认配置 =====================
const DEFAULT_CONFIG = {
apiProvider: 'openai', // openai | deepseek | qwen | zhipu | custom
apiKey: '',
apiBaseUrl: '', // 自定义时填写
model: '', // 留空则使用各服务商默认模型
autoAnswer: false, // 是否自动答题(无需点击按钮)
answerDelay: 800, // 自动答题延迟(ms)
showFloatBtn: true, // 显示悬浮按钮
temperature: 0.2,
};
// 各服务商预设
const PROVIDERS = {
openai: { name: 'OpenAI', baseUrl: 'https://api.openai.com/v1/chat/completions', defaultModel: 'gpt-4o-mini' },
deepseek: { name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1/chat/completions', defaultModel: 'deepseek-chat' },
qwen: { name: '通义千问(阿里)', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', defaultModel: 'qwen-turbo' },
zhipu: { name: '智谱GLM', baseUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', defaultModel: 'glm-4-flash' },
kimi: { name: 'Kimi(月之暗面)', baseUrl: 'https://api.moonshot.cn/v1/chat/completions', defaultModel: 'moonshot-v1-8k' },
custom: { name: '自定义', baseUrl: '', defaultModel: '' },
};
// ===================== 配置读写 =====================
function getConfig() {
const saved = GM_getValue('cxai_config', '{}');
try {
return Object.assign({}, DEFAULT_CONFIG, JSON.parse(saved));
} catch {
return { ...DEFAULT_CONFIG };
}
}
function saveConfig(cfg) {
GM_setValue('cxai_config', JSON.stringify(cfg));
}
// ===================== 样式注入 =====================
GM_addStyle(`
/* 悬浮按钮 */
#cxai-float-btn {
position: fixed;
bottom: 80px;
right: 20px;
z-index: 99999;
width: 52px;
height: 52px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
font-size: 22px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 15px rgba(102,126,234,0.5);
transition: all 0.3s ease;
border: none;
user-select: none;
}
#cxai-float-btn:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(102,126,234,0.7); }
/* 主面板遮罩 */
#cxai-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 100000;
display: none;
align-items: center;
justify-content: center;
}
#cxai-overlay.show { display: flex; }
/* 主面板 */
#cxai-panel {
background: #fff;
border-radius: 12px;
width: 520px;
max-width: 95vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
#cxai-panel .panel-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 16px 20px;
border-radius: 12px 12px 0 0;
display: flex;
align-items: center;
justify-content: space-between;
}
#cxai-panel .panel-header h2 { margin: 0; font-size: 16px; font-weight: 600; }
#cxai-panel .panel-close {
background: none;
border: none;
color: #fff;
font-size: 20px;
cursor: pointer;
line-height: 1;
padding: 0 4px;
}
#cxai-panel .panel-body { padding: 20px; }
/* 表单 */
.cxai-form-group { margin-bottom: 16px; }
.cxai-form-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
.cxai-form-group input,
.cxai-form-group select {
width: 100%;
box-sizing: border-box;
padding: 9px 12px;
border: 1.5px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #111827;
outline: none;
transition: border-color 0.2s;
}
.cxai-form-group input:focus,
.cxai-form-group select:focus { border-color: #667eea; }
.cxai-form-group .hint { font-size: 11px; color: #9ca3af; margin-top: 4px; }
/* 切换开关 */
.cxai-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f3f4f6;
}
.cxai-toggle-row:last-child { border-bottom: none; }
.cxai-toggle-label { font-size: 13px; color: #374151; font-weight: 500; }
.cxai-switch {
position: relative;
width: 42px;
height: 24px;
}
.cxai-switch input { opacity: 0; width: 0; height: 0; }
.cxai-slider {
position: absolute;
inset: 0;
background: #d1d5db;
border-radius: 24px;
cursor: pointer;
transition: 0.3s;
}
.cxai-slider:before {
content: '';
position: absolute;
width: 18px;
height: 18px;
left: 3px;
bottom: 3px;
background: #fff;
border-radius: 50%;
transition: 0.3s;
}
.cxai-switch input:checked + .cxai-slider { background: #667eea; }
.cxai-switch input:checked + .cxai-slider:before { transform: translateX(18px); }
/* 按钮 */
.cxai-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.cxai-btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
}
.cxai-btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
.cxai-btn-success {
background: linear-gradient(135deg, #11998e, #38ef7d);
color: #fff;
}
.cxai-btn-success:hover { opacity: 0.9; }
.cxai-btn-danger {
background: #ef4444;
color: #fff;
}
.cxai-btn-outline {
background: transparent;
border: 1.5px solid #d1d5db;
color: #374151;
}
.cxai-btn-outline:hover { border-color: #667eea; color: #667eea; }
/* 操作区 */
.cxai-action-row {
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
}
/* 日志 */
#cxai-log {
background: #1e1e2e;
color: #a6e3a1;
border-radius: 8px;
padding: 12px;
font-size: 12px;
font-family: 'Courier New', monospace;
max-height: 160px;
overflow-y: auto;
margin-top: 16px;
line-height: 1.6;
}
#cxai-log .log-error { color: #f38ba8; }
#cxai-log .log-warn { color: #fab387; }
#cxai-log .log-info { color: #89dceb; }
#cxai-log .log-ok { color: #a6e3a1; }
/* 进度条 */
#cxai-progress-bar {
height: 4px;
background: #e5e7eb;
border-radius: 4px;
margin-top: 12px;
overflow: hidden;
display: none;
}
#cxai-progress-bar.show { display: block; }
#cxai-progress-inner {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%;
transition: width 0.4s ease;
}
`);
// ===================== 日志 =====================
const logs = [];
function log(msg, type = 'info') {
const ts = new Date().toLocaleTimeString();
logs.push({ ts, msg, type });
if (logs.length > 100) logs.shift();
const el = document.getElementById('cxai-log');
if (el) {
el.innerHTML = logs.map(l =>
`
[${l.ts}] ${escHtml(l.msg)}
`
).join('');
el.scrollTop = el.scrollHeight;
}
console.log(`[CX-AI][${type}] ${msg}`);
}
function escHtml(s) {
return String(s).replace(/&/g, '&').replace(//g, '>');
}
// ===================== UI 构建 =====================
function buildUI() {
const cfg = getConfig();
// 遮罩
const overlay = document.createElement('div');
overlay.id = 'cxai-overlay';
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
// 悬浮按钮
const floatBtn = document.createElement('button');
floatBtn.id = 'cxai-float-btn';
floatBtn.title = '超星AI答题';
floatBtn.textContent = '🤖';
floatBtn.style.display = cfg.showFloatBtn ? 'flex' : 'none';
document.body.appendChild(floatBtn);
// 事件绑定
floatBtn.onclick = () => overlay.classList.add('show');
document.getElementById('cxai-close').onclick = () => overlay.classList.remove('show');
overlay.onclick = (e) => { if (e.target === overlay) overlay.classList.remove('show'); };
// 提供商切换
document.getElementById('cxai-provider').onchange = function () {
const v = this.value;
document.getElementById('cxai-custom-url-group').style.display = v === 'custom' ? '' : 'none';
const hint = document.getElementById('cxai-model-hint');
hint.textContent = v !== 'custom' ? `默认模型:${PROVIDERS[v].defaultModel}` : '';
};
// 初始化 hint
const initProvider = document.getElementById('cxai-provider').value;
if (initProvider !== 'custom') {
document.getElementById('cxai-model-hint').textContent = `默认模型:${PROVIDERS[initProvider].defaultModel}`;
}
// 保存
document.getElementById('cxai-save-btn').onclick = () => {
const newCfg = {
apiProvider: document.getElementById('cxai-provider').value,
apiKey: document.getElementById('cxai-apikey').value.trim(),
apiBaseUrl: document.getElementById('cxai-base-url').value.trim(),
model: document.getElementById('cxai-model').value.trim(),
temperature: parseFloat(document.getElementById('cxai-temperature').value) || 0.2,
autoAnswer: document.getElementById('cxai-auto').checked,
answerDelay: parseInt(document.getElementById('cxai-delay').value) || 800,
showFloatBtn: document.getElementById('cxai-float').checked,
};
saveConfig(newCfg);
floatBtn.style.display = newCfg.showFloatBtn ? 'flex' : 'none';
log('配置已保存 ✅', 'ok');
};
// 立即答题
document.getElementById('cxai-run-btn').onclick = () => {
const cfg = getConfig();
if (!cfg.apiKey) {
log('请先填写 API Key!', 'error');
return;
}
runAnswerAll(cfg);
};
// 清除标注
document.getElementById('cxai-clear-btn').onclick = () => {
// 清理旧标记(已移除该功能)
log('已清除所有答案标注', 'info');
};
}
// ===================== 题目抓取 =====================
/**
* 超星学习通题目选择器(覆盖主流页面结构)
*/
function getQuestions() {
// 策略1: 章节测验 / 作业 (.questionLi, .q_content) - 最常见
let items = document.querySelectorAll('.questionLi, .questionItem, .stem_wrap');
if (items.length > 0) return Array.from(items);
// 策略2: 考试页面
items = document.querySelectorAll('.TiMu, .examQuestion, .question-item');
if (items.length > 0) return Array.from(items);
// 策略3: 新版学习通
items = document.querySelectorAll('[data-question-id], .question-wrapper, .question-box');
if (items.length > 0) return Array.from(items);
// 策略4: 通用兜底
items = document.querySelectorAll('[class*="question"],[class*="Question"],[class*="timu"],[class*="TiMu"]');
return Array.from(items);
}
/**
* 从题目节点提取题干和选项
*/
function parseQuestion(node) {
// 题干 - 扩大选择器范围
const stemSelectors = [
'.stem_wrap .stem', '.q_content', '.questionContent',
'.examContent', '.TiMuContent', '.question-content',
'p.fontLabel', '.fontLabel', 'span[id*="title"]',
'.question-title', '.title', 'h3', 'h4',
'[class*="stem"]', '[class*="Stem"]',
];
let stemEl = null;
for (const sel of stemSelectors) {
stemEl = node.querySelector(sel);
if (stemEl) break;
}
if (!stemEl) stemEl = node;
// 清理题干文本(去掉题号、序号等)
let stemText = stemEl.innerText.trim()
.replace(/^\d+[..、\s]+/, '') // 去掉开头的 1. 2. 等
.replace(/^[((]\d+[))][..、\s]*/, '') // 去掉 (1) 等
.replace(/^【.*?】/, '') // 去掉【单选题】等标签
.trim();
// 题型判断 - 优先从文本内容判断
const typeEl = node.querySelector('[class*="type"],[class*="Type"],.question-type');
const typeText = (typeEl ? typeEl.innerText : '') + ' ' + stemText.slice(0, 20);
let qType = 'unknown';
if (/单选|single/i.test(typeText)) qType = 'single';
else if (/多选|multi|不定项/i.test(typeText)) qType = 'multi';
else if (/判断|true.*false|truefalse/i.test(typeText)) qType = 'judge';
else if (/填空|blank|fill/i.test(typeText)) qType = 'fill';
// 通过输入元素补判(更可靠)
const radios = node.querySelectorAll('input[type="radio"]');
const checks = node.querySelectorAll('input[type="checkbox"]');
const texts = node.querySelectorAll('input[type="text"], textarea');
if (qType === 'unknown') {
if (checks.length > 0 && checks.length > radios.length) qType = 'multi';
else if (radios.length > 0) qType = 'single';
else if (texts.length > 0) qType = 'fill';
}
// 选项
const options = [];
const optSelectors = [
'.answerBg', // 作业/考试页面最常见
'.q_answer li', '.answerList li', '.optionsList li',
'.option_item', 'ul.answer_list li',
'li[class*="option"]', 'li[class*="answer"]',
'.option', '.choice', '[class*="option"]',
];
let optEls = null;
for (const sel of optSelectors) {
const found = node.querySelectorAll(sel);
if (found.length > 0) { optEls = found; break; }
}
// 兜底:找 label 或者直接找 li
if (!optEls || optEls.length === 0) {
optEls = node.querySelectorAll('label');
}
if (!optEls || optEls.length === 0) {
optEls = node.querySelectorAll('li');
}
optEls.forEach(li => {
// 提取选项文本,去掉 A. B. 等前缀
const txt = li.innerText.trim()
.replace(/^[A-Da-d][..、\s]+/, '') // 去掉 A. B. 等
.trim();
if (txt && txt.length > 0 && txt !== stemText) {
options.push(txt);
}
});
// 去重
const uniqueOptions = [...new Set(options)];
return { node, stemText, qType, options: uniqueOptions };
}
// ===================== AI 调用 =====================
function buildPrompt(q) {
// 针对 Kimi 优化:更清晰的指令格式
let prompt = `请回答以下题目,只输出答案本身,不要任何解释。\n\n`;
if (q.qType === 'single') {
prompt += `【单选题】\n题目:${q.stemText}\n`;
if (q.options.length > 0) {
prompt += `\n选项:\n${q.options.map((o, i) => `${String.fromCharCode(65 + i)}. ${o}`).join('\n')}\n`;
}
prompt += `\n请只回答一个选项字母(A/B/C/D),不要加任何其他内容。`;
} else if (q.qType === 'multi') {
prompt += `【多选题】\n题目:${q.stemText}\n`;
if (q.options.length > 0) {
prompt += `\n选项:\n${q.options.map((o, i) => `${String.fromCharCode(65 + i)}. ${o}`).join('\n')}\n`;
}
prompt += `\n请回答所有正确选项的字母,用逗号分隔(如:A,C),不要加任何其他内容。`;
} else if (q.qType === 'judge') {
prompt += `【判断题】\n题目:${q.stemText}\n`;
prompt += `\n请回答"正确"或"错误",不要加任何其他内容。`;
} else if (q.qType === 'fill') {
prompt += `【填空题】\n题目:${q.stemText}\n`;
prompt += `\n请直接填写答案。如果有多个空,用"|||"分隔每个空的答案。`;
} else {
prompt += `【题目】\n${q.stemText}\n`;
if (q.options.length > 0) {
prompt += `\n选项:\n${q.options.map((o, i) => `${String.fromCharCode(65 + i)}. ${o}`).join('\n')}\n`;
}
prompt += `\n请直接给出答案。`;
}
return prompt;
}
// 用 fetch 作为后备(绕过 @connect 白名单限制)
function callAIviaFetch(url, body, cfg) {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${cfg.apiKey}`,
},
body,
}).then(r => {
if (!r.ok) {
return r.text().then(txt => {
throw new Error(`HTTP ${r.status}: ${txt.slice(0, 200)}`);
});
}
return r.json();
}).then(data => {
// 处理 Kimi 的错误格式
if (data.error) {
const errMsg = data.error.message || (typeof data.error === 'string' ? data.error : JSON.stringify(data.error));
throw new Error(errMsg);
}
// 标准 OpenAI 格式
let text = data.choices?.[0]?.message?.content?.trim() || '';
// Kimi 可能返回的字段差异兜底
if (!text && data.choices?.[0]?.text) {
text = data.choices[0].text.trim();
}
if (!text) throw new Error('AI 返回内容为空,请检查模型名称和 API Key 是否正确');
return text;
});
}
function callAI(prompt, cfg) {
const provider = PROVIDERS[cfg.apiProvider] || PROVIDERS.openai;
const url = cfg.apiProvider === 'custom' ? cfg.apiBaseUrl : provider.baseUrl;
const model = cfg.model || provider.defaultModel;
if (!url) return Promise.reject(new Error('API 地址未配置,请在配置面板填写'));
if (!cfg.apiKey) return Promise.reject(new Error('API Key 未填写'));
// Kimi 适配:使用更长的 max_tokens
const maxTokens = cfg.apiProvider === 'kimi' ? 512 : 256;
const body = JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }],
temperature: cfg.temperature || 0.1, // 降低温度让输出更确定
max_tokens: maxTokens,
});
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${cfg.apiKey}`,
},
data: body,
timeout: 60000, // Kimi 可能较慢,延长超时
onload(res) {
// url.not_found / 非2xx 时降级到 fetch
if (res.status === 0 || res.status === 404 || !res.responseText) {
log('GM请求被拦截,尝试降级到 fetch 模式...', 'warn');
callAIviaFetch(url, body, cfg).then(resolve).catch(reject);
return;
}
try {
const data = JSON.parse(res.responseText);
if (data.error) {
const msg = data.error.message || JSON.stringify(data.error);
reject(new Error(msg));
return;
}
let text = data.choices?.[0]?.message?.content?.trim() || '';
// Kimi 可能返回的字段差异兜底
if (!text && data.choices?.[0]?.text) {
text = data.choices[0].text.trim();
}
if (!text) { reject(new Error('AI 返回内容为空')); return; }
resolve(text);
} catch (e) {
reject(new Error('解析响应失败: ' + res.responseText.slice(0, 200)));
}
},
onerror(err) {
// url.not_found 本质是 onerror,直接降级
log('GM请求错误(可能是域名未加白名单),尝试降级到 fetch 模式...', 'warn');
callAIviaFetch(url, body, cfg).then(resolve).catch(e => {
// 详细错误提示
let help = '';
if (e.message.includes('401')) {
help = 'API Key 无效或已过期,请检查配置面板中的 API Key';
} else if (e.message.includes('404')) {
help = 'API 地址错误,请确认选择的 AI 服务商或自定义地址正确';
} else if (e.message.includes('429')) {
help = '请求太频繁或额度不足,请稍后再试';
} else if (e.message.includes('CORS') || e.message.includes('Failed to fetch')) {
help = '浏览器拦截了跨域请求。\n解决方法:\n1. 在 Tampermonkey 脚本设置中将"允许跨域请求"改为"始终允许"\n2. 或使用"自定义"模式,配合本地代理';
} else {
help = '请检查网络连接、API Key 和模型名称是否正确';
}
reject(new Error(`请求失败:${e.message}\n\n${help}`));
});
},
ontimeout() { reject(new Error('请求超时(60s),请检查网络或 API 地址是否正确')); },
});
});
}
// ===================== 答案填写 =====================
function fillAnswer(q, answer) {
let ans = answer.trim();
const node = q.node;
// 清理 AI 答案(去掉可能的解释性文字)
ans = ans
.replace(/^[((]?(?:答案|正确答案是|答案为|答案是)[::]?\s*/i, '') // 去掉"答案是:"
.replace(/[.。))]*$/, '') // 去掉结尾标点
.trim();
// 标记题目已处理
node.setAttribute('data-cxai-answered', 'true');
if (q.qType === 'single' || q.qType === 'judge') {
// 找到对应选项并点击
const optEls = getOptionElements(node);
const letter = ans.toUpperCase().replace(/[^A-Z正确错误是否]/g, '');
// 判断题特殊处理
if (q.qType === 'judge') {
const isTrue = /正确|true|是|对/.test(ans.toLowerCase());
optEls.forEach(el => {
const txt = el.innerText.trim();
if ((isTrue && /正确|true|是|对/.test(txt.toLowerCase())) ||
(!isTrue && /错误|false|否|错/.test(txt.toLowerCase()))) {
clickOption(el);
}
});
return;
}
// 单选按字母匹配 - 支持多种匹配方式
optEls.forEach((el, i) => {
// 方法1: 按索引匹配 A=0, B=1, C=2...
if (String.fromCharCode(65 + i) === letter[0]) {
clickOption(el);
return;
}
// 方法2: 检查 data 属性(如 data="A")
const span = el.querySelector('span[data]');
if (span && span.getAttribute('data') === letter[0]) {
clickOption(el);
return;
}
// 方法3: 检查选项文本是否以该字母开头
const txt = el.innerText.trim();
if (txt.startsWith(letter[0] + '.') || txt.startsWith(letter[0] + ' ')) {
clickOption(el);
}
});
} else if (q.qType === 'multi') {
const letters = ans.toUpperCase().match(/[A-Z]/g) || [];
const optEls = getOptionElements(node);
optEls.forEach((el, i) => {
// 方法1: 按索引匹配
if (letters.includes(String.fromCharCode(65 + i))) {
clickOption(el);
return;
}
// 方法2: 检查 data 属性
const span = el.querySelector('span[data]');
if (span && letters.includes(span.getAttribute('data'))) {
clickOption(el);
return;
}
// 方法3: 检查选项文本
const txt = el.innerText.trim();
letters.forEach(letter => {
if (txt.startsWith(letter + '.') || txt.startsWith(letter + ' ')) {
clickOption(el);
}
});
});
} else if (q.qType === 'fill') {
const blanks = node.querySelectorAll('input[type="text"], textarea');
const parts = ans.split(/\|\|\||\n/).map(s => s.trim()).filter(s => s);
blanks.forEach((input, i) => {
const val = parts[i] || parts[0] || ans;
setNativeValue(input, val);
});
}
}
function getOptionElements(node) {
// 策略1: 作业/考试页面 (.answerBg div,最常见)
let opts = node.querySelectorAll('.answerBg');
if (opts.length > 0) return Array.from(opts);
// 策略2: 标准章节测验 (.answer_list li)
opts = node.querySelectorAll('.answer_list li, .answerList li');
if (opts.length > 0) return Array.from(opts);
// 策略3: 其他 li 结构
const selectors = [
'.q_answer li', '.optionsList li',
'.option_item', 'li[class*="option"]', 'li[class*="answer"]',
];
for (const sel of selectors) {
const found = node.querySelectorAll(sel);
if (found.length > 0) return Array.from(found);
}
return Array.from(node.querySelectorAll('li'));
}
// 参考飘飘脚本优化的点击逻辑
function clickOption(el) {
if (!el) return;
// 标记正在自动填充,防止重复点击
if (el.getAttribute('data-filling') === 'true') return;
el.setAttribute('data-filling', 'true');
// 检查是否已选中(通过 aria-checked 或选中样式)
const isAlreadyChecked = el.getAttribute('aria-checked') === 'true' ||
el.querySelector('.check_answer, .check_answer_dx, input:checked');
if (isAlreadyChecked) {
el.removeAttribute('data-filling');
return;
}
// 直接点击元素(学习通主要响应 click 事件)
el.click();
// 视觉反馈
el.style.backgroundColor = '#dbeafe';
el.style.transition = 'background-color 0.3s';
setTimeout(() => { el.style.backgroundColor = ''; }, 600);
// 200ms 后移除标记
setTimeout(() => el.removeAttribute('data-filling'), 200);
}
// React 兼容赋值
function setNativeValue(el, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
|| Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(el, value);
} else {
el.value = value;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
// ===================== 主流程 =====================
async function runAnswerAll(cfg) {
const questions = getQuestions();
if (questions.length === 0) {
log('未检测到题目,请确认当前页面有题目内容', 'warn');
return;
}
log(`检测到 ${questions.length} 道题目,开始答题...`, 'info');
const progressBar = document.getElementById('cxai-progress-bar');
const progressInner = document.getElementById('cxai-progress-inner');
if (progressBar) progressBar.classList.add('show');
const parsed = questions.map(parseQuestion);
let ok = 0, fail = 0;
for (let i = 0; i < parsed.length; i++) {
const q = parsed[i];
if (progressInner) progressInner.style.width = `${Math.round(((i + 1) / parsed.length) * 100)}%`;
const shortStem = q.stemText.slice(0, 30).replace(/\s+/g, ' ');
log(`[${i + 1}/${parsed.length}] ${shortStem}...`, 'info');
try {
const prompt = buildPrompt(q);
const answer = await callAI(prompt, cfg);
log(` → AI答案:${answer}`, 'ok');
fillAnswer(q, answer);
ok++;
} catch (e) {
log(` → 失败:${e.message}`, 'error');
fail++;
}
// 延迟避免频控
if (i < parsed.length - 1) {
await sleep(cfg.answerDelay || 800);
}
}
if (progressBar) progressBar.classList.remove('show');
log(`答题完成!成功 ${ok} 题,失败 ${fail} 题`, ok > 0 ? 'ok' : 'error');
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
// ===================== 菜单命令 =====================
GM_registerMenuCommand('⚙️ 配置 AI 答题助手', () => {
document.getElementById('cxai-overlay')?.classList.add('show');
});
GM_registerMenuCommand('▶ 立即开始答题', () => {
const cfg = getConfig();
if (!cfg.apiKey) { alert('请先在配置面板中填写 API Key!'); return; }
runAnswerAll(cfg);
});
// ===================== 初始化 =====================
function init() {
// 等待页面渲染
setTimeout(() => {
buildUI();
log('超星AI答题助手已加载,点击 🤖 按钮配置并开始答题', 'info');
// 自动答题
const cfg = getConfig();
if (cfg.autoAnswer && cfg.apiKey) {
log('自动答题模式已开启,3秒后开始...', 'warn');
setTimeout(() => runAnswerAll(cfg), 3000);
}
}, 1500);
}
init();
})();