// ==UserScript==
// @name 芯位网课辅助 (Xinwei AutoStudy)
// @namespace https://github.com/wikkd/xinwei-autostudy
// @version 2.0.0
// @description 基于大语言模型的网课学习辅助:自动刷课 + AI 答题(单选/多选/判断/填空/主观,检索增强 + 本地答案本)。MIT 开源。
// @author wikkd
// @homepage https://github.com/wikkd/xinwei-autostudy
// @supportURL https://github.com/wikkd/xinwei-autostudy/issues
// @match https://www.beeline-ai.com/*
// @match https://beeline-ai.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect *
// @connect api.deepseek.com
// @connect api.siliconflow.cn
// @connect api.openai.com
// @connect html.duckduckgo.com
// @connect api.tavily.com
// @license MIT
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
let muteEnabled = true; // 运行期开关:是否静音视频
const CFG = {
PPT_DWELL: 5, // 静态/PPT 课件停留秒数(打开即算已观看)
MIN_WATCH: 30, // 启动后最少观看秒数(防过早跳章)
COOLDOWN: 15, // 跳章后冷却秒数
INTERVAL: 3, // 主循环 / 题目扫描间隔(秒)
BANK_MAX: 400, // 答案本最大条数(超出淘汰最早的)
DEFAULT_API_BASE: 'https://api.deepseek.com',
DEFAULT_MODEL: 'deepseek-chat',
DEFAULT_SEARCH_PROVIDER: 'duckduckgo', // duckduckgo(免密钥) / tavily(需Key)
DEFAULT_SEARCH_MODE: 'auto', // off / auto / always
SEARCH_MAX_CHARS: 3000,
};
const SK = {
apiKey: 'xa_apikey', apiBase: 'xa_apibase', model: 'xa_model',
autoAnswer: 'xa_autoanswer', autoPlay: 'xa_autoplay', autoSubmit: 'xa_autosubmit',
searchProvider: 'xa_search_provider', searchApiKey: 'xa_search_key', searchMode: 'xa_search_mode',
bank: 'xa_answer_bank', // 答案本:题目hash -> {a:答案, t:时间}
panelPos: 'xa_panel_pos', // 面板/迷你球拖动位置
minimized: 'xa_minimized', // 面板是否处于最小化
};
let conf = {};
function loadCfg() {
const g = (k, d) => GM_getValue(k, d);
const b = v => v === true || v === 'true';
conf = {
apiKey: g(SK.apiKey, ''),
apiBase: g(SK.apiBase, CFG.DEFAULT_API_BASE),
model: g(SK.model, CFG.DEFAULT_MODEL),
autoAnswer: b(g(SK.autoAnswer, false)),
autoPlay: b(g(SK.autoPlay, false)),
autoSubmit: b(g(SK.autoSubmit, true)),
searchProvider: g(SK.searchProvider, CFG.DEFAULT_SEARCH_PROVIDER),
searchApiKey: g(SK.searchApiKey, ''),
searchMode: g(SK.searchMode, CFG.DEFAULT_SEARCH_MODE),
};
}
function visible(el) {
if (!el || !el.getBoundingClientRect) return false;
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) return false;
const style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
return true;
}
function qsa(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
function qtext(el) { return ((el && el.textContent) || '').replace(/\s+/g, ' ').trim(); } // 压缩空白
function qtextAll(el) { return ((el && el.textContent) || '').replace(/\s+/g, ''); } // 去全部空白(精确比对用)
function muteAll() {
document.querySelectorAll('video').forEach(v => { if (muteEnabled && !v.muted) v.muted = true; });
}
function keepPlay() {
document.querySelectorAll('video, audio').forEach(v => {
if (v.paused && !v.ended && v.readyState >= 2) {
try { v.play().catch(() => {}); } catch (e) {}
}
});
try { Object.defineProperty(document, 'hidden', { get: () => false }); } catch (e) {}
try { Object.defineProperty(document, 'visibilityState', { get: () => 'visible' }); } catch (e) {}
try { document.hasFocus = function () { return true; }; } catch (e) {}
const randInt = (a, b) => Math.floor(Math.random() * (b - a) + a);
const x = randInt(50, window.innerWidth - 50 || 200);
const y = randInt(50, window.innerHeight - 50 || 200);
try {
document.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y,
movementX: randInt(-5, 5), movementY: randInt(-5, 5),
}));
} catch (e) {}
if (Math.random() > 0.75) {
try {
const t = document.elementFromPoint(x, y) || document.body;
t.dispatchEvent(new MouseEvent('click', {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y, button: 0,
}));
} catch (e) {}
}
if (Math.random() > 0.88) {
try {
const key = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp'][randInt(0, 6)];
document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
} catch (e) {}
}
if (Math.random() > 0.65) {
try { window.scrollBy({ top: randInt(-15, 15), left: randInt(-8, 8), behavior: 'instant' }); } catch (e) {}
}
}
function ensureVisible(el) {
if (!el) return;
try {
el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
} catch (e) {
try { el.scrollIntoView(); } catch (e2) {}
}
let p = el.parentElement, steps = 0;
while (p && steps < 6) {
if (p.scrollHeight > p.clientHeight) {
try { p.scrollTop = el.offsetTop - p.clientHeight / 2; } catch (e) {}
}
p = p.parentElement;
steps++;
}
}
function findNextMenuItem(activeNode) {
let cur = activeNode;
const LIMIT = 80;
for (let i = 0; i < LIMIT; i++) {
let sib = cur.nextElementSibling;
if (!sib && cur.parentElement) {
let p = cur.parentElement;
while (p && !p.nextElementSibling) p = p.parentElement;
if (p) sib = p.nextElementSibling;
}
if (!sib) break;
if (sib.matches && sib.matches('.el-sub-menu__title, .el-menu-item-group__title')) {
cur = sib;
continue;
}
const items = sib.querySelectorAll ? sib.querySelectorAll('li.el-menu-item') : [];
if (items.length > 0) {
let target = null;
for (const it of items) {
const txt = (it.textContent || '').trim();
if (txt.length === 0) continue;
const hasTime = /[\d::]/.test(txt) || it.querySelector('.time, .duration, [class*="time"]');
if (!hasTime) continue;
if (it.offsetHeight > 0 && it.offsetWidth > 0) { target = it; break; }
}
if (target) return target;
} else if (sib.matches && sib.matches('li.el-menu-item')) {
return sib;
}
cur = sib;
}
return null;
}
const XIA_QROOT = '.homework-single-selected, .homework-multiple-selected, .homework-true-or-false, .homework-cloze, .homework-question-editor';
function clickNextQuestionButton() {
const hit = el => /下一题|下一个/.test(qtext(el)) && !/上一题|提交|交卷|作业|保存/.test(qtext(el));
let btn = null;
for (const t of qsa('.toggle-button')) if (hit(t)) { btn = t; break; }
if (!btn) for (const t of qsa('button, [role="button"], [class*="next"]')) { if (visible(t) && hit(t)) { btn = t; break; } }
if (btn) {
try { btn.click(); } catch (e) {}
XIA_AI.log('→ 下一题 "' + qtext(btn).slice(0, 12) + '"', 'ok');
} else if (Date.now() - (XIA_AI._navErrAt || 0) > 10000) {
XIA_AI._navErrAt = Date.now();
XIA_AI.log('未找到下一题按钮', 'error');
}
return !!btn;
}
function homeworkWalker() {
const h = location.hash || '';
const now = Date.now();
if (h.indexOf('homeworkDetailPage') >= 0) { // 提交完成后的详情/成绩页:停留十几秒再返回,模拟人工节奏
if (now - XIA_AI._walkerNav < 12000) return true;
XIA_AI._walkerNav = now;
const link = exactText('a, button, [role="button"]', '作业考试');
if (link) {
XIA_AI.log('✅ 本份作业已提交,返回作业列表', 'ok');
XIA_AI.progress('✅ 已提交,返回列表中');
try { link.click(); } catch (e) {}
}
return true;
}
if (h.indexOf('homeworkPaperId') >= 0) return false;
if (h.indexOf('/homework') >= 0) { // 作业列表页
if (now - XIA_AI._walkerNav < 8000) return true;
XIA_AI._walkerNav = now;
const btn = exactText('button, [role="button"]', '做作业');
if (btn) {
XIA_AI.log('📖 打开下一份未完成作业', 'ok');
XIA_AI.progress('📖 打开下一份作业…');
try { btn.click(); } catch (e) {}
} else if (now - XIA_AI._walkerEmptyLog > 60000) {
XIA_AI._walkerEmptyLog = now;
XIA_AI.log('作业列表暂无未完成作业(全部完成或待批改)', 'info');
XIA_AI.progress('🏠 列表暂无未完成作业');
}
return true;
}
return false;
}
function exactText(selector, text) {
let hit = null;
qsa(selector).forEach(el => { if (!hit && visible(el) && qtextAll(el) === text) hit = el; });
return hit;
}
function questionRoot() {
let roots = qsa(XIA_QROOT).filter(el => visible(el) && el.offsetHeight > 8);
if (!roots.length) { // 兜底:.question-tag-score 是各题型组件共有的作用域标记,取其容器作为题目块
const seen = [];
qsa('.question-tag-score').forEach(tag => {
const box = tag.closest('.question-box') || tag.parentElement;
if (!box || seen.indexOf(box) >= 0) return;
if (!box.querySelector('.topic-title, .subject-title, .el-radio, .el-checkbox, input')) return;
if (visible(box) && box.offsetHeight > 8) seen.push(box);
});
roots = seen;
}
if (!roots.length) return null;
for (const r of roots) if (!isAnswered(r)) return r; // 优先返回未作答的(列表页自动顺延到下一题)
return roots[0];
}
function questionType(root) {
if (!root) return 'single';
const cls = root.className || '';
if (cls.indexOf('homework-multiple-selected') >= 0) return 'multi';
if (cls.indexOf('homework-true-or-false') >= 0) return 'bool';
if (cls.indexOf('homework-question-editor') >= 0) return 'subjective';
if (cls.indexOf('homework-cloze') >= 0) return 'cloze';
if (cls.indexOf('homework-single-selected') >= 0) return 'single';
if (root.querySelector('.el-checkbox')) return 'multi';
return 'single';
}
function isAnswered(root) {
if (!root) return false;
const t = questionType(root);
if (t === 'subjective') {
const ed = root.querySelector('[contenteditable="true"]');
return !!(ed && qtextAll(ed).length > 0);
}
if (t === 'cloze') return qsa('input', root).some(i => (i.value || '').trim());
return !!root.querySelector('.is-checked');
}
function letterOf(label) {
const m = label.querySelector('.index-name');
const nameLetter = m ? qtext(m).match(/[A-H]/) : null;
if (nameLetter) return nameLetter[0];
const input = label.querySelector('input[type="radio"], input[type="checkbox"]');
const v = input && input.value ? String(input.value).trim().toUpperCase() : '';
return /^[A-H]$/.test(v) ? v : null;
}
function allSelected(root, letters) {
if (!root) return false;
const labels = qsa('.el-radio, .el-checkbox', root);
return String(letters).toUpperCase().split('').every(ch => {
const hit = labels.find(l => letterOf(l) === ch);
if (!hit) return false;
const input = hit.querySelector('input[type="radio"], input[type="checkbox"]');
return hit.classList.contains('is-checked') || !!(input && input.checked);
});
}
function questionScore(root) {
const scope = root.querySelector('.question-tag-score') ? root : (root.closest('.question-box') || root.parentElement || root);
const m = scope.querySelector('.question-tag-score');
const s = m ? qtext(m).match(/(\d+)\s*分/) : null;
return s ? parseInt(s[1], 10) : 0;
}
function targetWords(score) {
if (!score || score <= 0) return 250;
if (score >= 20) return 600;
if (score >= 10) return 400;
if (score >= 5) return 250;
return 150;
}
function extractOptions(root) {
const opts = [];
qsa('.el-radio, .el-checkbox', root).forEach(label => {
if (!visible(label)) return;
const n = label.querySelector('.index-name');
const m = n ? qtext(n).match(/[A-H]/) : null;
let letter = m ? m[0] : String.fromCharCode(65 + opts.length);
const el = label.querySelector('.label, .el-radio__label, .el-checkbox__label');
const text = el ? qtext(el) : qtext(label).replace(/^[A-H][\.\.、\::\s]*/, '');
if (opts.some(o => o.letter === letter)) letter = String.fromCharCode(65 + opts.length); // 字母重复则按顺序补
opts.push({ letter: letter, text: text.slice(0, 120), node: label });
});
return opts;
}
function questionTitle(root) {
const t = root.querySelector('.topic-title, .subject-title, [class*="topic-title"], [class*="subject-title"]');
return (t ? qtext(t) : qtext(root))
.replace(/^\s*\d+[\.\.、]*/, '')
.replace(/\s*单选|\s*多选|\s*判断|\s*填空|\s*主观题/g, '')
.replace(/\s*\d+\s*分\s*/g, '')
.slice(0, 400);
}
const XIA_AI = {
lastQuestionHash: '',
answering: false,
answeringOnce: false,
stats: { answered: 0, cached: 0, submitted: 0, failed: 0 },
_lastNav: 0,
_navErrAt: 0,
_failHash: '',
_failCount: 0,
_skipHash: '',
_skipCount: 0,
_submitting: false,
_lastSubmitTry: 0,
_donePage: '',
_walkerNav: 0,
_walkerEmptyLog: 0,
_cfgError: null,
_cfgLogAt: 0,
_bank: null,
logLines: [],
log(msg, type) {
const line = '[' + new Date().toLocaleTimeString() + '] ' + msg;
XIA_AI.logLines.push({ text: line, type: type || 'info' });
if (XIA_AI.logLines.length > 200) XIA_AI.logLines.shift();
const box = document.getElementById('xa-ai-log');
if (box) {
box.innerHTML = XIA_AI.logLines.slice(-60).map(l => {
const col = l.type === 'error' ? '#dc2626' : l.type === 'ok' ? '#059669' : l.type === 'ai' ? '#7c3aed' : '#374151';
return '
' + l.text.replace(/';
}).join('');
box.scrollTop = box.scrollHeight;
}
console.log('[XIA] ' + msg);
},
progress(text) {
const el = document.getElementById('xa-progress');
if (el) el.textContent = text;
},
bump(k) {
if (k) XIA_AI.stats[k]++;
const el = document.getElementById('xa-stats');
if (el) {
const s = XIA_AI.stats;
el.textContent = '已答 ' + s.answered + ' · 缓存命中 ' + s.cached + ' · 已提交 ' + s.submitted;
}
},
bankGet(key) {
if (!XIA_AI._bank) {
try { XIA_AI._bank = JSON.parse(GM_getValue(SK.bank, '{}')) || {}; } catch (e) { XIA_AI._bank = {}; }
}
const hit = XIA_AI._bank[key];
return hit ? hit.a : null;
},
bankSet(key, answer) {
XIA_AI.bankGet(key); // 确保已加载
XIA_AI._bank[key] = { a: answer, t: Date.now() };
const keys = Object.keys(XIA_AI._bank);
if (keys.length > CFG.BANK_MAX) { // 超上限:淘汰最早写入的
keys.sort((x, y) => XIA_AI._bank[x].t - XIA_AI._bank[y].t);
keys.slice(0, keys.length - CFG.BANK_MAX).forEach(k => { delete XIA_AI._bank[k]; });
}
try { GM_setValue(SK.bank, JSON.stringify(XIA_AI._bank)); } catch (e) {}
},
grabQuestion() {
try {
const root = questionRoot();
if (!root) return null;
const type = questionType(root);
const title = questionTitle(root);
if (!title || title.length < 3) return null;
const score = questionScore(root);
if (type === 'subjective') return { question: title, options: [], node: root, type: type, score: score };
if (type === 'cloze') {
const blanks = qsa('input', root).filter(el => visible(el) && !/(radio|checkbox|hidden|file|button|submit)/i.test(el.type)).length;
if (!blanks) { XIA_AI.log('检测到填空题但未找到输入框', 'error'); return null; }
return { question: title, options: [], node: root, type: type, blanks: blanks, score: score };
}
const options = extractOptions(root);
if (!options.length) { XIA_AI.log('抓到题目但无选项: ' + title.slice(0, 60), 'error'); return null; }
return { question: title, options: options, node: root, type: (type === 'bool' ? 'single' : type), score: score };
} catch (e) {
XIA_AI.log('grabQuestion异常: ' + (e && e.message ? e.message : e), 'error');
return null;
}
},
needsSearch(question) {
const q = question || '';
if (/(世界|国际|全球|国外|海外|外国)/.test(q)) return true;
if (/(19|20)\d{2}\s*年|哪一年|成立于|创立于|发明|发现|首次|第一次|总统|总理|主席|首相|元首|首都|人口|GDP|经济总[量值]|世界杯|奥运|历史|地理|政治|名人|品牌|公司|企业|最新|目前|截至|现任|多少岁|面积|海拔|发明者|作者|画家|科学家/.test(q)) return true;
if (/\d{3,}/.test(q)) return true;
return /[A-Za-z]{3,}\s*(公司|大学|奖|协议|法案|组织)/.test(q);
},
buildPrompt(question, options, type, ctx, score, blanks) {
const opt = (options || []).map(o => o.letter + '. ' + o.text).join(' ');
let base;
if (type === 'subjective') {
const words = targetWords(score);
base = '你是考试答题助手。请回答下面的主观题' + (score > 0 ? '(' + score + '分)' : '') + '。要求:\n'
+ '- 直接输出答案正文,禁止任何开场白、引言或「以下是回答」之类的废话\n'
+ '- 用 3-6 个自然段展开论述:先亮明总观点,再分层论证,最后总结提升\n'
+ '- 篇幅约 ' + words + ' 字,论点要配论据(理论、事实、举措、例子),不要堆空洞套话\n'
+ '- 分点时用「第一/第二/第三」或「首先/其次/再次/最后」衔接,不要用 Markdown 符号和标题\n\n'
+ '题目:' + question;
} else if (type === 'cloze') {
base = '你是答题助手。下面是一道填空题,共 ' + blanks + ' 空。请按顺序给出每空的答案:每空一行、只写答案本身,'
+ '不要序号、不要解释、不要重复题干。\n\n题目:' + question;
} else if (type === 'multi') {
base = '你是答题助手。请分析以下多选题,输出所有正确选项的字母,多个字母直接拼接(如 ABC),不要有任何废话。\n\n题目:' + question + '\n\n选项:' + opt;
} else {
base = '你是答题助手。请分析以下题目和选项,直接输出正确选项的字母(如 A 或 B),只输出一个字母,不要有任何废话、不要解释、不要换行。\n\n题目:' + question + '\n\n选项:' + opt;
}
if (ctx) {
return '以下是检索到的相关资料(可能包含答案,请甄别并优先采信权威可靠来源;若资料与你的知识冲突,以资料为准):\n' + ctx + '\n\n==========\n\n' + base;
}
return base;
},
searchWeb(query, cb) {
const maxChars = CFG.SEARCH_MAX_CHARS;
const done = ctx => { try { cb(ctx || ''); } catch (e) {} };
if ((conf.searchProvider || 'duckduckgo') === 'tavily') {
if (!conf.searchApiKey) { XIA_AI.log('Tavily 需要 API Key,回退为不检索', 'error'); return done(''); }
GM_xmlhttpRequest({
method: 'POST', url: 'https://api.tavily.com/search', timeout: 15000,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ api_key: conf.searchApiKey, query: query, max_results: 5, search_depth: 'basic' }),
onload: r => {
try {
const j = JSON.parse(r.responseText);
done((j.results || []).map(x => (x.title ? '【' + x.title + '】' : '') + (x.content || '')).join('\n').slice(0, maxChars));
} catch (e) { XIA_AI.log('检索解析失败: ' + e.message, 'error'); done(''); }
},
onerror: () => { XIA_AI.log('检索网络错误', 'error'); done(''); },
ontimeout: () => { XIA_AI.log('检索超时', 'error'); done(''); },
});
} else {
GM_xmlhttpRequest({
method: 'GET', timeout: 8000,
url: 'https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query),
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' },
onload: r => {
try { // 结果摘要与标题正则合并提取
const out = [];
const re = /class="result__(?:snippet|a)"[^>]*>([\s\S]*?)<\/a>/g;
let m;
while ((m = re.exec(r.responseText || '')) !== null) {
const t = m[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
if (t) out.push(t);
}
done(out.join('\n').slice(0, maxChars));
} catch (e) { XIA_AI.log('检索解析失败: ' + e.message, 'error'); done(''); }
},
onerror: () => { XIA_AI.log('检索网络错误(DDG 可能被墙/需代理)', 'error'); done(''); },
ontimeout: () => { XIA_AI.log('检索超时(8s),已跳过检索直接作答', 'error'); done(''); },
});
}
},
parseAnswer(content, type) {
const c = String(content || '').trim();
if (!c) return null;
if (type === 'subjective') return c;
if (type === 'cloze') {
const lines = c.split(/\n+/).map(s => s.trim()).filter(Boolean);
const NUM_RE = /^\s*[((]?\d{1,2}\s*[\.、\).::)]\s*/;
const numbered = lines.length > 1 && lines.every(s => {
if (!NUM_RE.test(s)) return false;
const rest = s.replace(NUM_RE, '');
return rest.length > 0 && /[^\d\s\..、。::]/.test(rest);
});
const clean = lines
.map(s => (numbered ? s.replace(NUM_RE, '') : s).replace(/[\..、。]\s*$/, '').trim())
.filter(Boolean);
return clean.length ? clean.join('|') : null;
}
if (type === 'multi') {
const letters = c.match(/[A-H]/g);
return letters ? letters.filter((ch, i, a) => a.indexOf(ch) === i).join('') : null;
}
const m = c.match(/^\s*([A-H])\s*$/i)
|| c.match(/(?:正确答案|参考答案|答案|应选|选|答|answer|option|choice)\s*(?:is|[::是为])?\s*([A-H])(?![A-Za-z])/i)
|| c.match(/^\s*([A-H])[\s\.\.、::]/i)
|| c.match(/\b([A-H])\b/i);
return m ? m[1].toUpperCase() : null;
},
callAI(q, onResult) {
const type = q.type || 'single';
const isSubjective = type === 'subjective';
const apiKey = conf.apiKey;
if (!apiKey) { XIA_AI.log('未配置 API Key', 'error'); onResult(null); return; }
const base = (conf.apiBase || CFG.DEFAULT_API_BASE).replace(/\/+$/, '');
const endpoint = /\/v\d+\/?$/.test(base) ? base + '/chat/completions' : base + '/v1/chat/completions';
const model = conf.model || CFG.DEFAULT_MODEL;
const isReasoning = /reasoner|thinking|qwq|r1|o1|o3|o4/i.test(model);
const maxTokens = isSubjective ? (isReasoning ? 3000 : 2000) : type === 'cloze' ? 300 : (isReasoning ? 1024 : 16);
const timeout = (isSubjective || isReasoning) ? 60000 : 20000;
const needSearch = conf.searchMode === 'always'
|| (conf.searchMode === 'auto' && !isSubjective && type !== 'cloze' && XIA_AI.needsSearch(q.question));
const ask = (ctx, strict) => {
let prompt = XIA_AI.buildPrompt(q.question, q.options, type, ctx, q.score, q.blanks);
if (strict) {
prompt += type === 'cloze'
? '\n\n(重要:上一轮回复格式不符合要求。请严格每空一行、每行只写一个答案,不要任何其他文字。)'
: '\n\n(重要:上一轮回复格式不符合要求。只输出选项字母,不要输出任何其他文字。)';
}
XIA_AI.log('请求 ' + model + (ctx ? '(含检索资料)' : '') + (strict ? '(格式重试)' : ''), 'ai');
try {
GM_xmlhttpRequest({
method: 'POST', url: endpoint, timeout: timeout,
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
data: JSON.stringify({
model: model,
messages: [{ role: 'user', content: prompt }],
temperature: isSubjective ? 0.7 : 0.3,
max_tokens: maxTokens,
}),
onload: res => {
const status = res.status || res.statusCode;
if (status && status >= 400) {
const body = res.responseText || '';
XIA_AI.log('AI 响应异常 HTTP ' + status + ' | ' + body.slice(0, 200), 'error');
if (/invalid_request_error/i.test(body) && /model/i.test(body)) {
XIA_AI._cfgError = '模型名 "' + model + '" 无效,请修正面板中的模型名称';
} else if (status === 401) {
XIA_AI._cfgError = 'API Key 无效(401),请检查面板中的 Key';
} else if (status === 402) {
XIA_AI._cfgError = 'API 余额不足(402),请充值或更换接口';
}
if (XIA_AI._cfgError) {
XIA_AI.log('⛔ ' + XIA_AI._cfgError + ',已暂停自动答题', 'error');
XIA_AI.progress('⛔ 配置错误,已暂停');
}
onResult(null); return;
}
let content = '';
try {
const data = JSON.parse(res.responseText);
content = (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) || '';
} catch (e) { content = ''; }
if (!content) { XIA_AI.log('AI 响应无内容: ' + (res.responseText || '').slice(0, 200), 'error'); onResult(null); return; }
const answer = XIA_AI.parseAnswer(content, type);
if (!answer) {
if (!strict && !isSubjective) { ask(ctx, true); return; } // 强调格式重试一次
XIA_AI.log('AI 返回无法解析: "' + content.trim().slice(0, 80) + '"', 'error');
onResult(null); return;
}
XIA_AI.log('AI 答案 → ' + (isSubjective ? answer.slice(0, 80) + '…' : answer), 'ok');
onResult(answer);
},
onerror: e => { XIA_AI.log('网络错误(发送失败): ' + (e && e.error ? e.error : ''), 'error'); onResult(null); },
ontimeout: () => { XIA_AI.log('请求超时(>' + timeout / 1000 + 's): ' + endpoint, 'error'); onResult(null); },
});
} catch (e) {
XIA_AI.log('LLM 请求异常: ' + e.message, 'error');
onResult(null);
}
};
if (needSearch) {
XIA_AI.log('🔍 先检索相关资料…', 'ai');
XIA_AI.searchWeb(q.question, ctx => ask(ctx || '', false));
} else {
ask('', false);
}
},
forceClick(el) {
if (!el) return;
try { el.click(); } catch (e) {}
try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } catch (e) {}
try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
const input = el.querySelector && el.querySelector('input[type="radio"], input[type="checkbox"]');
if (input) {
try { input.checked = true; } catch (e) {}
try { input.click(); } catch (e) {}
try { input.dispatchEvent(new MouseEvent('click', { bubbles: true })); } catch (e) {}
try { input.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
const group = el.closest && el.closest('.el-radio-group, .el-checkbox-group, [role="radiogroup"], [role="group"]');
if (group) {
try { group.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {}
try { group.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
}
}
},
fillChoice(letters, root, onDone) {
const qEl = root || questionRoot();
if (!qEl) { XIA_AI.log('页面已无题目容器,无法点击', 'error'); if (onDone) onDone(false); return; }
const isMulti = questionType(qEl) === 'multi';
const labels = qsa('.el-radio, .el-checkbox', qEl);
const arr = String(letters).toUpperCase().replace(/[^A-H]/g, '').split('');
let i = 0;
const step = () => {
if (i >= arr.length) { if (onDone) onDone(true); return; }
const ch = arr[i++];
let hit = null;
for (const l of labels) if (letterOf(l) === ch) { hit = l; break; }
if (!hit) hit = labels[ch.charCodeAt(0) - 65]; // 位置兜底:第 i 个选项 = 字母 A+i
if (!hit) { XIA_AI.log('找不到选项 ' + ch + ' 的节点', 'error'); step(); return; }
if (isMulti) {
const input = hit.querySelector('input[type="radio"], input[type="checkbox"]');
if (input && !input.checked) {
try { input.click(); } catch (e) { try { XIA_AI.forceClick(hit); } catch (e2) {} }
}
} else {
const inner = hit.querySelector('.el-radio__inner, .el-checkbox__inner');
if (inner) XIA_AI.forceClick(inner);
XIA_AI.forceClick(hit);
}
XIA_AI.log('点击选项 ' + ch, 'ok');
setTimeout(step, isMulti ? 350 : 150);
};
step();
},
fillCloze(answer, root, onDone) {
const inputs = qsa('input', root).filter(el => visible(el) && !el.disabled && !el.readOnly && !/(radio|checkbox|hidden|file|button|submit)/i.test(el.type));
const parts = String(answer).split('|');
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
let ok = 0;
inputs.slice(0, parts.length).forEach((input, i) => {
const v = parts[i];
try { setter.set.call(input, v); } catch (e) { try { input.value = v; } catch (e2) {} }
try { input.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {}
try { input.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
if ((input.value || '') === v) ok++;
});
if (ok > 0) XIA_AI.log('填空题已填 ' + ok + '/' + inputs.length + ' 空', 'ok');
else XIA_AI.log('填空题填入失败(输入框未接受内容)', 'error');
if (onDone) onDone(ok > 0);
},
answerSubjective(answer, onDone) {
const editor = document.querySelector('[data-slate-editor]')
|| document.querySelector('div.w-e-text-container [contenteditable="true"]')
|| document.querySelector('[contenteditable="true"]');
if (!editor) {
XIA_AI.log('未找到主观题编辑器', 'error');
if (onDone) onDone(false);
return;
}
try { editor.focus(); } catch (e) {}
try { editor.click(); } catch (e) {}
try { editor.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); } catch (e) {}
try { editor.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); } catch (e) {}
try { editor.dispatchEvent(new FocusEvent('focus', { bubbles: true })); } catch (e) {}
setTimeout(() => {
const sel = window.getSelection();
sel.removeAllRanges();
const range = document.createRange();
range.selectNodeContents(editor);
sel.addRange(range);
const paras = String(answer).split(/\n+/).map(s => s.trim()).filter(Boolean);
const normLen = () => (editor.textContent || '').replace(/\s+/g, '').length;
const expected = paras.join('').replace(/\s+/g, '').length;
const bi = (inputType, data) => {
try {
editor.dispatchEvent(new InputEvent('beforeinput', {
bubbles: true, cancelable: true, inputType: inputType, data: data,
}));
} catch (e) {}
};
let len = normLen();
for (let i = 0; i < paras.length; i++) {
if (i > 0) bi('insertParagraph', null);
bi('insertText', paras[i]);
if (normLen() === len) {
try { if (i > 0) document.execCommand('insertParagraph', false); } catch (e) {}
try { document.execCommand('insertText', false, paras[i]); } catch (e) {}
}
len = normLen();
}
try { editor.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {}
const after = normLen();
let ok = false;
if (after > 0 && after >= expected * 0.6) {
XIA_AI.log('主观题答案已填入(约' + after + '字): ' + paras[0].slice(0, 50) + (paras[0].length > 50 ? '…' : ''), 'ok');
ok = true;
} else if (after > 0) {
XIA_AI.log('主观题填入不完整(目标约' + expected + '字,实际' + after + '字),请人工检查补全', 'info');
} else {
XIA_AI.log('主观题填入失败(编辑器未响应合成输入),请手动作答', 'error');
}
if (onDone) onDone(ok);
}, 200);
},
verifyAndAdvance(q, hash, answer) {
setTimeout(() => {
let ok = false;
if (q.type === 'cloze') ok = qsa('input', q.node).some(inp => (inp.value || '').trim());
else ok = allSelected(q.node, answer);
if (XIA_AI._failHash !== hash) XIA_AI._failCount = 0;
if (ok) {
XIA_AI._failHash = '';
XIA_AI._failCount = 0;
XIA_AI.bankSet(hash, answer); // 成功回填 → 存入答案本
XIA_AI.lastQuestionHash = '';
XIA_AI.bump('answered');
XIA_AI.progress('✅ 已答 ' + XIA_AI.stats.answered + ' 题');
XIA_AI.log('回填已生效,跳转下一题', 'ok');
clickNextQuestionButton();
XIA_AI.answering = false;
return;
}
XIA_AI._failHash = hash;
XIA_AI._failCount++;
if (XIA_AI._failCount >= 3) {
XIA_AI._failHash = '';
XIA_AI._failCount = 0;
XIA_AI.lastQuestionHash = hash; // 标记为已处理,避免同题无限重答
XIA_AI.log('回填连续 3 次未生效,跳过本题(可手动作答)', 'error');
} else {
XIA_AI.lastQuestionHash = '';
XIA_AI.log('回填未生效,下一轮重试 (' + XIA_AI._failCount + '/3)', 'error');
}
XIA_AI.answering = false;
}, 800);
},
tryAutoSubmit() {
const now = Date.now();
if (XIA_AI._submitting) return;
if (XIA_AI._lastSubmitTry && now - XIA_AI._lastSubmitTry < 30000) return;
XIA_AI._lastSubmitTry = now;
let btn = null;
qsa('button, [role="button"]').forEach(el => {
if (btn || !visible(el)) return;
if (qtextAll(el) === '提交作业') btn = el;
});
if (!btn) return;
if (btn.disabled || (btn.className || '').indexOf('is-disabled') >= 0) {
XIA_AI.log('「提交作业」按钮不可用(可能有题目未作答),30s 后重试', 'info');
return;
}
XIA_AI._submitting = true;
XIA_AI.log('📝 全部答完,自动提交作业...', 'ok');
XIA_AI.progress('📝 自动提交中…');
try { btn.click(); } catch (e) {}
setTimeout(() => {
const dlg = qsa('.el-message-box, .el-dialog, .el-popconfirm').find(d => visible(d));
if (dlg) {
const msgEl = dlg.querySelector('.el-message-box__message, .el-dialog__body') || dlg;
const msg = qtextAll(msgEl);
if (/未(作答|回答|完成)|尚未(作答|完成)|还有\d*题/.test(msg)) {
XIA_AI.log('平台提示有题目未作答,已取消自动提交,请人工补答', 'error');
const cancel = dlg.querySelector('.el-message-box__btns .el-button:not(.el-button--primary), .el-dialog__footer .el-button:not(.el-button--primary)')
|| qsa('button', dlg).find(b => /^(取消|关闭)$/.test(qtextAll(b)));
try { if (cancel) cancel.click(); } catch (e) {}
XIA_AI._submitting = false;
return;
}
const confirm = dlg.querySelector('.el-message-box__btns .el-button--primary, .el-dialog__footer .el-button--primary')
|| qsa('button', dlg).find(b => /^(确定|确认|提交)$/.test(qtextAll(b)));
if (confirm && !confirm.disabled) {
try { confirm.click(); } catch (e) {}
XIA_AI.log('已点击确认提交', 'ok');
} else {
XIA_AI.log('弹窗中未找到确认按钮,请人工提交', 'error');
XIA_AI._submitting = false;
return;
}
} else {
XIA_AI.log('未出现确认弹窗,可能已直接提交', 'info');
}
setTimeout(() => {
const toast = qsa('.el-message').map(m => m.textContent || '').join(' ');
if (/提交成功|成功/.test(toast)) {
XIA_AI._donePage = location.hash; // 本页已提交完成,交给 walker 接管,不再自动跳题
XIA_AI.bump('submitted');
XIA_AI.log('🎉 作业提交成功', 'ok');
XIA_AI.progress('🎉 提交成功');
} else {
XIA_AI.log('提交结果未确认(无成功提示),请人工核对', 'info');
}
XIA_AI._submitting = false;
}, 2500);
}, 1200);
},
answerOnce() {
if (XIA_AI.answeringOnce) { XIA_AI.log('正在执行单次答题,请稍候', 'error'); return; }
const q = XIA_AI.grabQuestion();
if (!q || !q.question || (q.type !== 'subjective' && q.type !== 'cloze' && q.options.length === 0)) {
XIA_AI.log('当前页面未找到可答题目', 'error');
return;
}
XIA_AI.answeringOnce = true;
XIA_AI.log('🎯 单次答题: ' + q.question.slice(0, 60) + (q.question.length > 60 ? '…' : ''), 'ok');
XIA_AI.callAI(q, answer => {
XIA_AI.answeringOnce = false;
if (!answer) return;
if (q.type === 'subjective') {
XIA_AI.answerSubjective(answer, ok => XIA_AI.log(ok ? '单次答题(主观)完成' : '主观题填入失败', ok ? 'ok' : 'error'));
} else if (q.type === 'cloze') {
XIA_AI.fillCloze(answer, q.node, ok => XIA_AI.log(ok ? '单次答题(填空)完成' : '填空填入失败', ok ? 'ok' : 'error'));
} else {
XIA_AI.fillChoice(answer, q.node, ok => XIA_AI.log(ok ? '单次答题完成' : '回填失败', ok ? 'ok' : 'error'));
}
});
},
scan() {
if (!conf.autoAnswer || XIA_AI.answering) return;
if (XIA_AI._cfgError) { // 配置熔断:暂停扫描,面板改正任一 AI 配置项后自动恢复
const now = Date.now();
if (now - XIA_AI._cfgLogAt > 30000) {
XIA_AI._cfgLogAt = now;
XIA_AI.log('⏸ 已暂停:' + XIA_AI._cfgError, 'error');
}
return;
}
try { if (homeworkWalker()) return; } catch (e) { console.log('[XIA-walker] 异常', e); }
if (XIA_AI._donePage && location.hash === XIA_AI._donePage) return; // 本卷刚提交成功,等 walker 接管
const q = XIA_AI.grabQuestion();
if (!q || !q.question) return;
if (q.type !== 'subjective' && q.type !== 'cloze' && q.options.length === 0) return;
const hash = q.question.slice(0, 100) + '|'
+ (q.options.length ? q.options.map(o => o.letter + o.text.slice(0, 30)).join(',') : (q.type === 'cloze' ? 'cloze' : 'subj'));
if (hash === XIA_AI.lastQuestionHash) return;
if (isAnswered(q.node)) {
const now = Date.now();
if (XIA_AI._lastNav && now - XIA_AI._lastNav < 2500) return;
if (XIA_AI._skipHash === hash) XIA_AI._skipCount++;
else { XIA_AI._skipHash = hash; XIA_AI._skipCount = 0; }
if (XIA_AI._skipCount >= 2) {
if (XIA_AI._skipCount === 2) XIA_AI.log('已是最后一题或无法翻页,停止自动跳题', 'info');
if (conf.autoSubmit) XIA_AI.tryAutoSubmit(); // 卡在最后一题:触发自动提交(30s 节流重试)
return;
}
XIA_AI._lastNav = now;
XIA_AI.lastQuestionHash = '';
XIA_AI.log('题目已作答,跳到下一题', 'info');
clickNextQuestionButton();
return;
}
XIA_AI.answering = true;
XIA_AI.progress('📋 ' + q.question.slice(0, 24));
XIA_AI.log('📋 抓到题目: ' + q.question.slice(0, 60) + (q.question.length > 60 ? '…' : '')
+ ' [' + q.type + (q.score ? ' ' + q.score + '分' : '') + ']');
if (q.options.length) XIA_AI.log('选项 ' + q.options.length + ' 个: ' + q.options.map(o => o.letter).join(' '));
const cached = (q.type !== 'subjective' && q.type !== 'cloze') ? XIA_AI.bankGet(hash) : null;
if (cached) {
XIA_AI.log('💾 命中答案本,直接回填: ' + cached, 'ok');
XIA_AI.bump('cached');
XIA_AI.lastQuestionHash = hash;
XIA_AI.fillChoice(cached, q.node, () => XIA_AI.verifyAndAdvance(q, hash, cached));
return;
}
XIA_AI.callAI(q, ans => {
if (!ans) {
XIA_AI.lastQuestionHash = ''; // 未答出:重置 hash 下一轮重试
XIA_AI.bump('failed');
XIA_AI.log('AI 未返回答案,下一轮将重试本题', 'error');
XIA_AI.answering = false;
return;
}
XIA_AI.lastQuestionHash = hash; // 标记已处理,避免同页重复作答
if (q.type === 'subjective') {
XIA_AI.answerSubjective(ans, ok => {
if (ok) {
XIA_AI.bankSet(hash, ans);
XIA_AI.bump('answered');
XIA_AI.progress('✅ 已答 ' + XIA_AI.stats.answered + ' 题');
XIA_AI.log('主观题答案已填入,2s 后跳转下一题', 'ok');
setTimeout(() => { clickNextQuestionButton(); XIA_AI.answering = false; }, 2000);
} else {
XIA_AI.log('主观题未成功填入,已停止自动跳转,请人工处理', 'error');
XIA_AI.answering = false;
}
});
} else if (q.type === 'cloze') {
XIA_AI.fillCloze(ans, q.node, () => XIA_AI.verifyAndAdvance(q, hash, ans));
} else {
XIA_AI.fillChoice(ans, q.node, () => XIA_AI.verifyAndAdvance(q, hash, ans));
}
});
},
testGrab() {
const q = XIA_AI.grabQuestion();
if (!q || !q.question) { XIA_AI.log('[测试] 未抓到题目(grabQuestion=null)', 'error'); return; }
XIA_AI.log('[测试] 题目 [' + q.type + (q.score ? ' ' + q.score + '分' : '') + ']: ' + q.question.slice(0, 150), 'ai');
if (q.options.length) q.options.forEach(o => XIA_AI.log('[测试] 选项 ' + o.letter + ': ' + o.text.slice(0, 60), 'ai'));
else if (q.type === 'cloze') XIA_AI.log('[测试] 填空 ' + q.blanks + ' 空', 'ai');
else XIA_AI.log('[测试] 无选项(主观题属正常)', 'ai');
},
diagnose() {
const out = [];
const push = s => { out.push(s); XIA_AI.log(s, 'ai'); };
push('🩺 诊断 ' + location.href.slice(0, 70));
const roots = qsa(XIA_QROOT);
push('① .homework-*根=' + roots.length + (roots.length ? '' : ' → 走兜底'));
roots.slice(0, 5).forEach((r, i) => push(' [' + i + '] class="' + (r.className || '').slice(0, 60) + '" h=' + r.offsetHeight + ' 类型=' + questionType(r)));
const tags = qsa('.question-tag-score');
push('② .question-tag-score=' + tags.length);
if (tags.length) {
const p = tags[0].closest('.question-box') || tags[0].parentElement;
if (p) push(' 兜底容器 class="' + (p.className || '').slice(0, 60) + '" 含题干=' + !!p.querySelector('.topic-title, .subject-title'));
}
push('③ div.question=' + qsa('div.question').length + '(旧结构,应为0)');
push('④ iframe=' + qsa('iframe').length + ' radio=' + qsa('.el-radio').length + ' checkbox=' + qsa('.el-checkbox').length);
const root = questionRoot();
if (!root) { push('❌ 未定位到题目容器'); console.log('[XIA-diagnose]\n' + out.join('\n')); return; }
push('⑤ 选中容器 class="' + (root.className || '').slice(0, 60) + '" 类型=' + questionType(root) + ' 已答=' + isAnswered(root));
push('⑥ 题干=' + (questionTitle(root) || '(空)').slice(0, 90));
const opts = extractOptions(root);
push('⑦ 选项=' + opts.length);
opts.slice(0, 8).forEach(o => push(' ' + o.letter + ': ' + o.text.slice(0, 45)));
console.log('[XIA-diagnose]\n' + out.join('\n'));
},
cruise() {
const link = exactText('a, button, [role="button"], li.el-menu-item, .el-sub-menu__title', '作业考试')
|| qsa('a, button, li.el-menu-item, .el-sub-menu__title').find(el => visible(el) && /作业考试|作业与考试|课程作业/.test(qtext(el)));
if (link) {
XIA_AI.log('🧭 已进入作业列表,巡航开始', 'ok');
XIA_AI.progress('🧭 作业巡航中');
try { link.click(); } catch (e) {}
if (!conf.autoAnswer) { // 顺手把自动答题开关打开,巡航才能真正跑起来
const sw = document.getElementById('xa-autoanswer-switch');
if (sw) sw.click();
}
} else {
XIA_AI.log('未找到「作业考试」入口,请先在课程菜单里手动展开一次', 'error');
}
},
};
const PANEL_CSS = `
#xa-panel{position:fixed;top:16px;left:16px;width:300px;background:#fff;border-radius:14px;
box-shadow:0 10px 32px rgba(0,0,0,.14),0 2px 8px rgba(0,0,0,.06);z-index:99999;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow:hidden;transition:opacity .25s,transform .25s;}
#xa-panel.xa-hidden{opacity:0;transform:scale(.85);pointer-events:none;}
.xa-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;
background:linear-gradient(135deg,#3b82f6,#8b5cf6);color:#fff;font-size:15px;font-weight:700;letter-spacing:.5px;cursor:move;user-select:none;}
.xa-min-btn{width:22px;height:22px;border-radius:6px;border:none;background:rgba(255,255,255,.2);
color:#fff;font-size:14px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s;}
.xa-min-btn:hover{background:rgba(255,255,255,.35);}
.xa-tabs{display:flex;background:#f9fafb;border-bottom:1px solid #e5e7eb;}
.xa-tab{flex:1;padding:9px 4px;font-size:12px;font-weight:600;color:#6b7280;text-align:center;cursor:pointer;transition:all .15s;border-bottom:2px solid transparent;}
.xa-tab:hover{color:#3b82f6;}
.xa-tab.active{color:#3b82f6;border-bottom-color:#3b82f6;background:#fff;}
.xa-body{padding:14px;display:flex;flex-direction:column;gap:10px;max-height:640px;overflow-y:auto;}
.xa-tab-pane{display:none;flex-direction:column;gap:10px;}
.xa-tab-pane.active{display:flex;}
.xa-status{font-size:12px;color:#6b7280;text-align:center;padding:6px 8px;border-radius:6px;background:#f3f4f6;transition:color .2s,background .2s;}
.xa-status.running{color:#059669;background:#d1fae5;}
.xa-status.paused{color:#d97706;background:#fef3c7;}
.xa-btn{width:100%;padding:9px 0;border:none;border-radius:10px;font-size:13px;font-weight:600;
cursor:pointer;transition:all .2s;letter-spacing:.3px;color:#fff;}
.xa-btn:disabled{opacity:.55;cursor:wait;}
.xa-once{background:linear-gradient(135deg,#0ea5e9,#06b6d4);}
.xa-once:hover{box-shadow:0 4px 12px rgba(59,130,246,.4);transform:translateY(-1px);}
.xa-once:active{transform:scale(.97);}
.xa-about{font-size:12px;color:#374151;line-height:1.8;max-height:350px;overflow-y:auto;
padding-right:4px;word-break:break-word;}
.xa-about h4{font-size:14px;color:#1e40af;margin:0 0 6px;}
.xa-about p{margin:0 0 8px;}
.xa-stop{background:#f3f4f6;color:#374151;}
.xa-stop:hover{background:#e5e7eb;}
.xa-mute{background:#f3f4f6;color:#374151;}
.xa-mute:hover{background:#e5e7eb;}
.xa-mute.muted{background:#fee2e2;color:#dc2626;}
.xa-input{width:100%;padding:8px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;
font-family:inherit;outline:none;transition:border-color .2s;box-sizing:border-box;}
.xa-input:focus{border-color:#3b82f6;box-shadow:0 0 0 2px rgba(59,130,246,.12);}
.xa-row{display:flex;align-items:center;gap:8px;}
.xa-switch{position:relative;width:42px;height:22px;border-radius:11px;background:#d1d5db;cursor:pointer;transition:background .2s;flex-shrink:0;}
.xa-switch.on{background:linear-gradient(135deg,#10b981,#059669);}
.xa-switch::after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;
background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2);transition:transform .2s;}
.xa-switch.on::after{transform:translateX(20px);}
.xa-led{display:inline-block;width:8px;height:8px;border-radius:50%;background:#9ca3af;margin-right:6px;vertical-align:middle;}
.xa-led.ok{background:#10b981;box-shadow:0 0 6px rgba(16,185,129,.6);}
.xa-led.err{background:#dc2626;}
.xa-ai-log{background:#0f172a;color:#e2e8f0;border-radius:8px;padding:10px;font-size:11px;
font-family:'SF Mono','Menlo','Consolas',monospace;height:240px;min-height:140px;max-height:480px;
resize:vertical;overflow-y:auto;line-height:1.5;}
.xa-ai-log::-webkit-scrollbar{width:4px;}
.xa-ai-log::-webkit-scrollbar-thumb{background:#475569;border-radius:2px;}
#xa-mini{position:fixed;top:16px;left:16px;width:44px;height:44px;border-radius:50%;
background:linear-gradient(135deg,#3b82f6,#8b5cf6);color:#fff;font-size:18px;font-weight:700;
display:none;align-items:center;justify-content:center;cursor:move;z-index:99999;
box-shadow:0 4px 16px rgba(59,130,246,.35);transition:transform .2s;user-select:none;}
#xa-mini:hover{transform:scale(1.1);}
#xa-mini.show{display:flex;}
.xa-sec{font-size:11px;color:#94a3b8;font-weight:700;letter-spacing:.5px;margin:2px 0 -2px;}
.xa-btnrow{display:flex;gap:8px;}
.xa-btnrow .xa-btn{flex:1;padding:8px 0;font-size:12px;letter-spacing:0;}
.xa-progress{font-size:11px;color:#2563eb;background:#eff6ff;border:1px solid #dbeafe;border-radius:8px;
padding:6px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.xa-stats{font-size:11px;color:#64748b;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;
padding:6px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.xa-loghead{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:#6b7280;font-weight:700;}
.xa-loghead button{border:none;background:#f3f4f6;color:#6b7280;font-size:11px;border-radius:6px;padding:3px 10px;cursor:pointer;transition:background .2s;}
.xa-loghead button:hover{background:#e5e7eb;color:#374151;}
`;
let panel = null;
let mini = null;
let statusEl = null;
let aiLed = null;
let aiStatusText = null;
function setStatus(text, cls) {
if (!statusEl) return;
statusEl.textContent = text;
statusEl.className = 'xa-status' + (cls ? ' ' + cls : '');
}
function setAIStatus(type, text) {
if (!aiLed || !aiStatusText) return;
if (type === true) aiLed.className = 'xa-led ok';
else if (type === false) aiLed.className = 'xa-led err';
else aiLed.className = 'xa-led';
aiStatusText.textContent = text || '';
}
function restorePos(el) {
try {
const all = JSON.parse(GM_getValue(SK.panelPos, '{}'));
const p = all && all[el.id];
if (p && typeof p.l === 'number') { el.style.left = p.l + 'px'; el.style.top = p.t + 'px'; }
} catch (e) {}
}
function savePos(el) {
try {
const all = JSON.parse(GM_getValue(SK.panelPos, '{}'));
all[el.id] = { l: parseInt(el.style.left) || 0, t: parseInt(el.style.top) || 0 };
GM_setValue(SK.panelPos, JSON.stringify(all));
} catch (e) {}
}
function buildPanel() {
const style = document.createElement('style');
style.textContent = PANEL_CSS;
document.head.appendChild(style);
panel = document.createElement('div');
panel.id = 'xa-panel';
panel.innerHTML = `
👋 关于本脚本
本脚本以 MIT 协议开源发布,源码与构建流程完全公开,详见仓库 LICENSE 与 README。
1. 为什么没有内置 AI?
为了避免密钥泄露风险并长期维护,脚本采用【单机自填版】:你需要在面板里填写自己的大模型 API Key(如 DeepSeek、SiliconFlow 等)。脚本不会上报任何数据。
2. 推荐的白嫖方案
新用户可在 硅基流动 (SiliconFlow)、DeepSeek 官方 等平台注册,通常会获赠大量免费 Token,足够完成课程学习。
3. 友情提示
本脚本仅供计算机自动化技术交流与学习,请在遵守平台规则与当地法律的前提下使用。AI 不保证 100% 正确率,刷完课建议自行复习一遍,使用风险请自行承担。
4. 反馈与更新
代码由社区协作维护。遇到 bug 可在 GitHub Issues 反馈。网课平台经常改版,若某天失效我会跟进修复。
📜 运行日志
`;
document.body.appendChild(panel);
restorePos(panel);
const ver = (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) ? 'v' + GM_info.script.version : '';
const verEl = panel.querySelector('#xa-ver');
if (verEl && ver) verEl.textContent = ver;
mini = document.createElement('div');
mini.id = 'xa-mini';
mini.textContent = '芯';
document.body.appendChild(mini);
restorePos(mini);
}
function makeDraggable(dragEl, handleEl) {
let dragging = false, moved = false, startX, startY, origLeft, origTop;
handleEl.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
if (e.target.closest('button, .xa-min-btn, .xa-switch, input, .xa-btn')) return;
e.preventDefault();
dragging = true;
moved = false;
startX = e.clientX;
startY = e.clientY;
origLeft = parseInt(dragEl.style.left) || dragEl.getBoundingClientRect().left;
origTop = parseInt(dragEl.style.top) || dragEl.getBoundingClientRect().top;
dragEl.style.transition = 'none';
dragEl.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (dx || dy) moved = true;
const maxX = window.innerWidth - dragEl.offsetWidth;
const maxY = window.innerHeight - dragEl.offsetHeight;
dragEl.style.left = Math.max(0, Math.min(origLeft + dx, maxX)) + 'px';
dragEl.style.top = Math.max(0, Math.min(origTop + dy, maxY)) + 'px';
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
dragEl.style.cursor = '';
dragEl.style.transition = '';
if (moved) savePos(dragEl);
});
}
function bindUI() {
let aiScanTimer = null;
function startAIScan() {
if (aiScanTimer) return;
setTimeout(() => { XIA_AI.scan(); }, 500);
aiScanTimer = setInterval(() => { XIA_AI.scan(); }, 3000);
}
function stopAIScan() {
if (aiScanTimer) { clearInterval(aiScanTimer); aiScanTimer = null; }
}
const $ = s => panel.querySelector(s);
statusEl = $('#xa-status');
aiLed = $('#xa-ai-led');
aiStatusText = $('#xa-ai-status-text');
const apikeyInput = $('#xa-apikey');
const apibaseInput = $('#xa-apibase');
const modelInput = $('#xa-model');
const searchModeInput = $('#xa-search-mode');
const searchProviderInput = $('#xa-search-provider');
const searchApiKeyInput = $('#xa-search-key');
const autoSwitch = $('#xa-autoanswer-switch');
const playSwitch = $('#xa-autoplay-switch');
qsa('.xa-tab').forEach(tab => {
tab.addEventListener('click', () => {
qsa('.xa-tab, .xa-tab-pane').forEach(el => el.classList.remove('active'));
tab.classList.add('active');
const pane = panel.querySelector('[data-pane="' + tab.dataset.tab + '"]');
if (pane) pane.classList.add('active');
});
});
$('.xa-min-btn').addEventListener('click', () => {
panel.classList.add('xa-hidden');
mini.classList.add('show');
GM_setValue(SK.minimized, true);
});
mini.addEventListener('click', () => {
panel.classList.remove('xa-hidden');
mini.classList.remove('show');
GM_setValue(SK.minimized, false);
});
const saveInput = (key, el) => {
const save = () => {
conf[key] = el.value.trim();
GM_setValue(SK[key], conf[key]);
if (XIA_AI._cfgError) { XIA_AI._cfgError = null; XIA_AI.log('配置已更新,自动答题恢复', 'ok'); }
};
el.addEventListener('change', save);
el.addEventListener('blur', save);
};
saveInput('apiKey', apikeyInput);
saveInput('apiBase', apibaseInput);
saveInput('model', modelInput);
saveInput('searchMode', searchModeInput);
saveInput('searchProvider', searchProviderInput);
saveInput('searchApiKey', searchApiKeyInput);
apikeyInput.addEventListener('input', () => {
conf.apiKey = apikeyInput.value.trim();
setAIStatus(conf.apiKey ? true : null, conf.apiKey ? 'AI 引擎已就绪' : '等待配置 Key');
});
apikeyInput.value = conf.apiKey;
apibaseInput.value = conf.apiBase;
modelInput.value = conf.model;
searchModeInput.value = conf.searchMode;
searchProviderInput.value = conf.searchProvider;
searchApiKeyInput.value = conf.searchApiKey;
setAIStatus(conf.apiKey ? true : null, conf.apiKey ? 'AI 引擎已就绪' : '等待配置 Key');
autoSwitch.addEventListener('click', () => {
conf.autoAnswer = !conf.autoAnswer;
GM_setValue(SK.autoAnswer, conf.autoAnswer);
refreshAISwitch();
});
function refreshAISwitch() {
if (conf.autoAnswer) {
autoSwitch.classList.add('on');
XIA_AI.log('自动答题已开启(每 ' + CFG.INTERVAL + 's 扫描页面)', 'ok');
startAIScan();
} else {
autoSwitch.classList.remove('on');
stopAIScan();
XIA_AI.log('自动答题已关闭', 'info');
}
}
const submitSwitch = $('#xa-autosubmit-switch');
submitSwitch.addEventListener('click', () => {
conf.autoSubmit = !conf.autoSubmit;
GM_setValue(SK.autoSubmit, conf.autoSubmit);
refreshSubmitSwitch();
});
function refreshSubmitSwitch() {
if (conf.autoSubmit) {
submitSwitch.classList.add('on');
XIA_AI.log('答完自动提交已开启(有未答题会自动取消提交)', 'ok');
} else {
submitSwitch.classList.remove('on');
XIA_AI.log('答完自动提交已关闭', 'info');
}
}
function refreshPlaySwitch() {
if (conf.autoPlay) {
playSwitch.classList.add('on');
startAuto();
} else {
playSwitch.classList.remove('on');
stopAuto();
}
}
playSwitch.addEventListener('click', () => {
conf.autoPlay = !conf.autoPlay;
GM_setValue(SK.autoPlay, conf.autoPlay);
refreshPlaySwitch();
});
$('#xa-answer-once').addEventListener('click', e => {
const b = e.currentTarget;
b.disabled = true;
XIA_AI.answerOnce();
setTimeout(() => { b.disabled = false; }, 3000);
});
$('#xa-cruise').addEventListener('click', () => { XIA_AI.cruise(); });
$('#xa-test-grab').addEventListener('click', () => { XIA_AI.testGrab(); });
$('#xa-diagnose').addEventListener('click', () => { XIA_AI.diagnose(); });
$('#xa-clear-log').addEventListener('click', () => {
XIA_AI.logLines = [];
const box = document.getElementById('xa-ai-log');
if (box) box.innerHTML = '
日志已清空
';
});
const muteBtn = $('.xa-mute');
muteBtn.addEventListener('click', () => {
muteEnabled = !muteEnabled;
if (muteEnabled) { muteAll(); muteBtn.classList.remove('muted'); muteBtn.textContent = '🔇 关闭静音'; }
else { document.querySelectorAll('video').forEach(v => { if (v.muted) v.muted = false; }); muteBtn.classList.add('muted'); muteBtn.textContent = '🔊 开启声音'; }
});
XIA_AI.bump(); // 初始化统计显示
refreshPlaySwitch();
refreshAISwitch();
refreshSubmitSwitch();
}
let scriptStart = Date.now();
let cooldownUntil = 0;
let pptSince = 0;
let tick = 0;
let consecutiveJumps = 0;
let mainLoopId = null;
function isStaticOrPptSection() {
for (const v of document.querySelectorAll('video')) {
if (visible(v)) return false; // 有可见视频 => 不是静态课件
}
for (const f of document.querySelectorAll('iframe')) {
const src = (f.src || '').toLowerCase();
if (src && /powerpointframe|ppt|slide|preview|doc|courseware|resource|office|viewer/.test(src)) return true;
}
return false;
}
function openSidebarSwitch() {
const btn = document.querySelector('.course-sidebar-switch');
if (btn && visible(btn)) { btn.click(); return true; }
return false;
}
function isSectionComplete() {
const now = Date.now();
if (now < cooldownUntil) return false;
const active = document.querySelector('li.el-menu-item.is-active');
if (active) {
if (active.querySelector('.inProgress-icon, .unStart-icon')) return false;
const done = active.querySelector('.done-icon, .icon-done, .el-icon-check, .el-icon-circle-check, .el-icon-check-circle, .is-done');
if (done && visible(done)) { console.log('[芯位] done-icon → 完成'); return true; }
}
if (isStaticOrPptSection()) {
if (pptSince === 0) pptSince = now;
if (now - pptSince > CFG.PPT_DWELL * 1000) { console.log('[芯位] PPT/静态课件停留' + CFG.PPT_DWELL + 's → 完成'); return true; }
return false;
}
pptSince = 0;
if (now - scriptStart < CFG.MIN_WATCH * 1000) return false;
for (const v of document.querySelectorAll('video')) {
if (!visible(v)) continue;
if (v.duration > 0 && (v.ended || v.currentTime >= v.duration - 0.5)) { console.log('[芯位] 视频播放完毕 → 完成'); return true; }
}
return false;
}
function clickNextChapter() {
const rights = document.querySelectorAll('.right, div.right');
for (const r of rights) {
if (!visible(r)) continue;
if ((r.textContent || '').includes('观看下一章节')) {
setTimeout(() => {
const target = r.closest('button, a, [role="button"], [class*="btn"], [class*="Btn"]') || r;
target.click();
}, 800 + Math.random() * 1000);
console.log('[芯位] 点击 "观看下一章节" 按钮');
cooldownUntil = Date.now() + CFG.COOLDOWN * 1000;
pptSince = 0; consecutiveJumps++;
return true;
}
}
return false;
}
function navigateToNextSection() {
openSidebarSwitch();
const active = document.querySelector('li.el-menu-item.is-active');
if (!active) { console.log('[芯位] 无高亮节点'); return false; }
const next = findNextMenuItem(active);
if (!next) { console.log('[芯位] 已是最后一节'); return false; }
const cur = (active.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 24);
const nxt = (next.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 28);
console.log('[芯位] 穿透搜索: "' + cur + '" → "' + nxt + '"');
ensureVisible(next);
setTimeout(() => {
if (!visible(next)) ensureVisible(next);
next.click();
cooldownUntil = Date.now() + CFG.COOLDOWN * 1000;
pptSince = 0; consecutiveJumps++;
console.log('[芯位] 点击完成');
}, 400);
return true;
}
function mainTick() {
tick++;
muteAll();
keepPlay();
if (tick % 10 === 0) {
const active = document.querySelector('li.el-menu-item.is-active');
const cur = active ? (active.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 20) : '(无)';
const cd = Math.max(0, Math.ceil((cooldownUntil - Date.now()) / 1000));
console.log('[芯位] #' + tick + ' | 当前: "' + cur + '" | cd=' + cd + 's | 连跳=' + consecutiveJumps);
}
if (!isSectionComplete()) { consecutiveJumps = 0; return; }
if (Date.now() < cooldownUntil) return;
if (consecutiveJumps >= 3) { console.log('[芯位] 连续跳转' + consecutiveJumps + '次,暂停50s'); cooldownUntil = Date.now() + 50000; consecutiveJumps = 0; return; }
console.log('[芯位] 触发跳转');
if (clickNextChapter()) return;
setTimeout(() => { if (!clickNextChapter()) navigateToNextSection(); }, 5000);
}
function startAuto() {
if (mainLoopId) return;
mainLoopId = setInterval(mainTick, CFG.INTERVAL * 1000);
setStatus('正在运行', 'running');
console.log('[芯位] 自动刷课已启动');
XIA_AI.log('🚀 自动挂机已启动,每 ' + CFG.INTERVAL + ' 秒扫描一次页面', 'ok');
}
function stopAuto() {
if (mainLoopId) { clearInterval(mainLoopId); mainLoopId = null; }
setStatus('已暂停', 'paused');
console.log('[芯位] 自动刷课已停止');
XIA_AI.log('⏸ 挂机已停止', 'info');
}
function initPopupAutoReload() {
const RELOAD_COOLDOWN = 20000; // 20s 冷却,防止刷新死循环
setInterval(() => {
const last = Number(GM_getValue('xa_last_reload_at', 0));
if (Date.now() - last < RELOAD_COOLDOWN) return;
const text = (document.body && document.body.innerText) || '';
if (text.indexOf('学习状态异常') !== -1 || text.indexOf('请刷新后继续学习') !== -1) {
GM_setValue('xa_last_reload_at', Date.now());
console.log('[芯位] 检测到学习状态异常弹窗,3秒后自动刷新页面');
setTimeout(() => { location.reload(); }, 3000);
}
}, 3000);
}
if (window.xaScriptLoaded) return;
window.xaScriptLoaded = true;
if (window.self !== window.top) return;
loadCfg();
buildPanel();
makeDraggable(panel, panel.querySelector('.xa-header'));
makeDraggable(mini, mini);
bindUI();
if (GM_getValue(SK.minimized, false)) {
panel.classList.add('xa-hidden');
mini.classList.add('show');
}
muteAll();
XIA_AI.log('本脚本以 MIT 协议开源发布,详见仓库 LICENSE。请遵守平台规则与当地法律使用。', 'info');
initPopupAutoReload();
console.log('[XinweiAutoStudy] 已加载');
})();