// ==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 = `

🤖 超星AI答题助手 v2.0

兼容 OpenAI 格式的任意接口地址
密钥仅保存在本地浏览器,不会上传任何服务器

🚀 自动答题(进入页面自动运行)
🎈 显示悬浮按钮
[系统] 就绪,等待操作...
`; 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(); })();