// ==UserScript==
// @name 广开外置大脑 v5.87
// @namespace http://tampermonkey.net/
// @version 5.87.0
// @description 自动形考答题、本地题库、记录答案
// @author xx
// @match *://*.ougd.cn/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @run-at document-start
// @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js
// ==/UserScript==
(function() {
'use strict';
// 绕过webdriver检测(必须在页面加载前执行)
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
// ==================== 配置 ====================
const CONFIG = {
delay: 1500,
delayBetweenQuestions: 500,
debug: true,
targetScore: 95,
coze: {
botId: '2253351410218857',
apiUrl: 'https://api.coze.cn/v3/chat'
}
};
// ==================== 工具函数 ====================
const log = (...args) => CONFIG.debug && console.log('%c[广开助手]', 'background:#007bff;color:white;padding:2px 6px;border-radius:3px', ...args);
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const getPageParam = (name) => new URL(location.href).searchParams.get(name);
// ==================== 数据存储 ====================
const getData = (key, def = null) => GM_getValue(key, def);
const setData = (key, val) => GM_setValue(key, val);
const getBank = () => getData('questionBank', {});
const saveBank = (bank) => setData('questionBank', bank);
const getBankCount = () => Object.keys(getBank()).length;
const getProgress = () => getData('progress', { finishedCourses: [], finishedQuizzes: [], finishedDiscussions: [] });
const saveProgress = (p) => setData('progress', p);
// ==================== 题库功能 ====================
function importBank(jsonStr) {
try {
const newData = JSON.parse(jsonStr);
const oldBank = getBank();
let added = 0, updated = 0;
if (Array.isArray(newData)) {
for (const item of newData) {
if (item.question && item.answer) {
const key = item.question.trim();
const value = typeof item.answer === 'string' ? item.answer : item.answer;
if (!oldBank[key]) {
oldBank[key] = value;
added++;
}
}
}
} else {
for (let key in newData) {
const value = newData[key];
if (/^\d+$/.test(key)) continue;
if (typeof value === 'object' && value.answer) {
if (!oldBank[key]) {
oldBank[key] = value.answer;
added++;
}
} else if (typeof value === 'string') {
if (!oldBank[key]) {
oldBank[key] = value;
added++;
} else {
const oldIsLetter = /^[A-Fa-f]+$/.test(oldBank[key]);
const newIsLetter = /^[A-Fa-f]+$/.test(value);
if (newIsLetter && !oldIsLetter) {
oldBank[key] = value;
updated++;
}
}
}
}
}
saveBank(oldBank);
return { success: true, added, updated, total: Object.keys(oldBank).length };
} catch (e) {
return { success: false, error: e.message };
}
}
function cleanText(text) {
return text ? text.replace(/[,。、;:""''!?()\s—–-]/g, '').trim() : '';
}
// ==================== AI答题 ====================
async function callAI(question, options) {
const token = getData('cozeToken', '');
if (!token) return null;
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'POST',
url: CONFIG.coze.apiUrl,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
data: JSON.stringify({
bot_id: CONFIG.coze.botId,
user_id: 'auto_answer',
stream: false,
additional_messages: [{
role: 'user',
content: `请回答这道题,只返回答案(选择题返回字母如A,填空题返回答案文本):\n\n题目:${question}\n\n选项:\n${options ? options.join('\n') : '无选项(可能是填空题)'}`,
content_type: 'text'
}]
}),
onload: (res) => {
try {
const json = JSON.parse(res.responseText);
const messages = json.data?.messages || [];
for (let msg of messages) {
if (msg.type === 'answer' || msg.role === 'assistant') {
const content = msg.content || '';
// 可能是字母或文字答案
const letterMatch = content.match(/^[A-Fa-f]$/);
if (letterMatch) {
const answer = letterMatch[0].toUpperCase();
const bank = getBank();
bank[question] = answer;
saveBank(bank);
return resolve(answer);
}
// 文字答案(填空题等)
const textAnswer = content.trim().split('\n')[0].replace(/[。.!!??,,]/g, '').trim();
if (textAnswer) {
const bank = getBank();
bank[question] = textAnswer;
saveBank(bank);
return resolve(textAnswer);
}
}
}
} catch (e) {}
resolve(null);
},
onerror: () => resolve(null),
timeout: 30000
});
});
}
// ==================== 页面识别 ====================
const PageType = {
MY: 'my',
COURSE_USER: 'course_user',
LEARNING_CONTENT: 'learning_content',
QUIZ_VIEW: 'quiz_view',
QUIZ_RESULT: 'quiz_result',
QUIZ_ATTEMPT: 'quiz_attempt',
QUIZ_SUMMARY: 'quiz_summary',
QUIZ_REVIEW: 'quiz_review',
FORUM_VIEW: 'forum_view',
FORUM_DISCUSS: 'forum_discuss',
FORUM_POST: 'forum_post'
};
function detectPageType() {
const url = location.href;
if (url.includes('/my/') || url === 'https://course.ougd.cn/') return PageType.MY;
// 区分形成性考核和学习内容页面
if (url.includes('/course/view.php')) {
const activeTab = document.querySelector('.nav-link.active .tab_content span');
const tabText = activeTab ? activeTab.textContent.trim() : '';
if (tabText === '形成性考核') {
return PageType.COURSE_USER;
}
return PageType.LEARNING_CONTENT;
}
if (url.includes('/course/user.php')) return PageType.COURSE_USER;
if (url.includes('/quiz/view.php')) {
const pageText = document.body.innerText;
if (pageText.match(/最高分[::]\s*\d+\.?\d*\s*\/\s*\d+\.?\d*/)) {
return PageType.QUIZ_RESULT;
}
return PageType.QUIZ_VIEW;
}
if (url.includes('/quiz/attempt.php')) return PageType.QUIZ_ATTEMPT;
if (url.includes('/quiz/summary.php')) return PageType.QUIZ_SUMMARY;
if (url.includes('/quiz/review.php')) return PageType.QUIZ_REVIEW;
if (url.includes('/forum/view.php')) return PageType.FORUM_VIEW;
if (url.includes('/forum/discuss.php')) return PageType.FORUM_DISCUSS;
if (url.includes('/forum/post.php')) return PageType.FORUM_POST;
return null;
}
// ==================== 答题核心 ====================
/**
* 从页面选项区域提取字母标签的实际位置映射
* 返回:{ A: { index: 0, element: ... }, B: { index: 1, element: ... }, ... }
*/
function extractOptionPositions(container, questionType) {
const selector = questionType === 'multiple' ? 'input[type="checkbox"]' : 'input[type="radio"]';
const options = container.querySelectorAll(selector);
const positions = {};
log(`找到 ${options.length} 个选项输入框`);
// 【v5.87关键修复】从.qtext提取完整选项文本
// 页面结构:.qtext里的
标签包含"A. 现有人力资源的存量"格式
// .answer里的.r0/.r1只包含字母"A"
const qtext = container.querySelector('.qtext');
const optionTexts = []; // 存储从.qtext提取的选项文本
if (qtext) {
// 【v5.87修复】支持两种格式:
// 格式1: 每个
一个选项 "A. 政治因素"
// 格式2: 所有选项在一个
里 "A. 政治因素 B. 经济因素 C. 竞争者"
const pTags = qtext.querySelectorAll('p');
pTags.forEach(p => {
const text = p.textContent?.trim() || '';
// 检查是否包含多个选项(通过匹配多个"A. "格式)
const multiMatch = text.match(/[A-Fa-f][..、\s][^A-Fa-f]+/g);
if (multiMatch && multiMatch.length > 1) {
// 格式2: 多个选项在一个字符串里
log(`检测到合并格式,共${multiMatch.length}个选项`);
multiMatch.forEach(match => {
const cleanMatch = match.trim();
const letterMatch = cleanMatch.match(/^([A-Fa-f])[..、\s]/);
if (letterMatch) {
const letter = letterMatch[1].toUpperCase();
const content = cleanMatch.replace(/^[A-Fa-f][..、\s]+/, '').trim();
optionTexts.push({ letter, content, fullText: cleanMatch });
log(`从合并文本提取: ${letter} → "${content.substring(0, 30)}..."`);
}
});
} else if (/^[A-Fa-f][..、\s]/.test(text)) {
// 格式1: 单个选项
const letterMatch = text.match(/^([A-Fa-f])[..、\s]/);
if (letterMatch) {
const letter = letterMatch[1].toUpperCase();
const content = text.replace(/^[A-Fa-f][..、\s]+/, '').trim();
optionTexts.push({ letter, content, fullText: text });
log(`从.qtext提取: ${letter} → "${content.substring(0, 30)}..."`);
}
}
});
}
// 如果.qtext里没有选项文本,尝试从.answer区域提取
if (optionTexts.length === 0) {
log('⚠ .qtext未找到选项文本,尝试从.answer提取');
const answerArea = container.querySelector('.answer');
if (answerArea) {
const rContainers = answerArea.querySelectorAll('.r0, .r1');
rContainers.forEach((r, i) => {
const text = r.textContent?.trim() || '';
if (/^[A-Fa-f]/.test(text)) {
const letterMatch = text.match(/^([A-Fa-f])/);
if (letterMatch) {
optionTexts.push({
letter: letterMatch[1].toUpperCase(),
content: text.replace(/^[A-Fa-f][..、\s]*/, '').trim(),
fullText: text
});
}
}
});
}
}
log(`提取到 ${optionTexts.length} 个选项文本`);
// 遍历checkbox,建立映射
options.forEach((opt, i) => {
// 找到选项标签容器
let label = opt.closest('label');
if (!label) {
label = opt.closest('.r0, .r1') || opt.parentElement;
}
// 跳过"清空我的选择"等非选项元素
const text = label?.innerText?.trim() || '';
if (text.includes('清空') || text.includes('重置')) {
log(`跳过非选项: "${text.substring(0,20)}..."`);
return;
}
let letter = null;
let content = '';
// 方法1:从.r0/.r1容器提取字母
const rContainer = opt.closest('.r0, .r1');
if (rContainer) {
// 查找单独显示字母的元素
const children = rContainer.children;
for (const child of children) {
const childText = child.textContent?.trim() || '';
if (/^[A-Fa-f]$/.test(childText)) {
letter = childText.toUpperCase();
break;
}
}
// 如果没找到,从整个容器文本提取
if (!letter) {
const containerText = rContainer.textContent?.trim() || '';
const letterMatch = containerText.match(/^([A-Fa-f])/);
if (letterMatch) {
letter = letterMatch[1].toUpperCase();
}
}
}
// 方法2:使用默认索引位置
if (!letter) {
const defaultLetters = ['A', 'B', 'C', 'D', 'E', 'F'];
letter = defaultLetters[i];
log(`⚠ 使用默认位置映射: 第${i+1}个选项 → ${letter}`);
}
// 从optionTexts中查找对应的内容
// 按字母匹配
for (const optText of optionTexts) {
if (optText.letter === letter) {
content = optText.content;
break;
}
}
// 如果按字母没找到,按索引匹配
if (!content && optionTexts[i]) {
content = optionTexts[i].content;
log(`按索引匹配: 第${i+1}个选项 → "${content.substring(0, 20)}..."`);
}
// 最后的备选:使用容器文本
if (!content) {
content = text.replace(/^[A-Fa-f][..、\s]*/, '').trim();
}
positions[letter] = {
index: i,
element: opt,
content: content,
fullText: text
};
log(`✓ 选项映射: ${letter} → "${content.substring(0, 30)}..."`);
});
log('所有选项映射:', Object.keys(positions).join(', '));
return positions;
}
function fillAnswer(container, answer, questionType) {
const answerUpper = answer.toUpperCase().trim();
log('填写答案:', answer, '类型:', questionType);
// 填空题处理
if (questionType === 'fill') {
return fillBlankAnswer(container, answer);
}
// 判断题
if (questionType === 'judge') {
const options = container.querySelectorAll('input[type="radio"]');
for (const opt of options) {
const label = opt.closest('label') || opt.parentElement;
const text = label?.innerText?.trim() || '';
const isCorrect = (answerUpper === '对' || answerUpper === 'A');
const isWrong = (answerUpper === '错' || answerUpper === 'B');
if ((isCorrect && (text === '对' || text.includes('正确'))) ||
(isWrong && (text === '错' || text.includes('错误')))) {
opt.click();
log('✓ 点击判断题:', text);
return true;
}
}
return false;
}
// 下拉选择框题型(案例阅读题等)
if (questionType === 'dropdown') {
return fillDropdownAnswer(container, answer);
}
// ========== 单选题和多选题的核心逻辑 ==========
// 1. 提取页面选项的字母位置映射
const positions = extractOptionPositions(container, questionType);
log('页面选项位置:', Object.keys(positions).join(''));
// 2. 解析答案
let answerLetters = null; // 字母(如 'B' 或 'ACD')
let answerContent = null; // 选项内容(用于内容匹配)
// 检查是否有选项内容(格式: "字母.内容" 或 "字母、内容" 或 "字母|内容")
const contentMatch = answer.match(/^[A-Fa-f][..、|]\s*(.+)$/);
if (contentMatch) {
answerContent = contentMatch[1].trim();
answerLetters = answer.match(/^[A-Fa-f]/)[0].toUpperCase();
log(`✓ 答案解析: 字母=${answerLetters}, 内容="${answerContent}"`);
}
// 检查增强格式 "字母|选项内容"
else if (answer.match(/^[A-Fa-f]+\|.+$/)) {
const enhancedMatch = answer.match(/^([A-Fa-f]+)\|(.+)$/);
answerLetters = enhancedMatch[1].toUpperCase();
answerContent = enhancedMatch[2];
log(`✓ 增强格式答案: 字母=${answerLetters}, 内容=${answerContent.substring(0,20)}...`);
}
// 纯字母格式
else if (/^[A-Fa-f]+$/.test(answer)) {
answerLetters = answer.toUpperCase();
log(`✓ 纯字母答案: ${answerLetters}`);
// 【v5.87关键】尝试把字母转换为选项内容,以应对选项顺序打乱
const letterArr = answerLetters.split('');
const contentArr = [];
for (const letter of letterArr) {
if (positions[letter] && positions[letter].content) {
contentArr.push(positions[letter].content);
}
}
// 如果能获取到所有字母对应的内容,使用内容匹配
if (contentArr.length === letterArr.length && contentArr.length > 0) {
answerContent = contentArr.join('、');
log(`✓ 字母转内容: ${answerLetters} → "${answerContent.substring(0,50)}..."`);
}
}
// 纯文字答案(不包含字母)
else if (!answer.match(/^[A-Fa-f]/)) {
answerContent = answer.trim();
log(`✓ 纯文字答案: ${answerContent.substring(0,20)}...`);
}
// 其他格式,尝试提取字母和内容
else {
const firstLetterMatch = answer.match(/^([A-Fa-f])/);
if (firstLetterMatch) {
answerLetters = firstLetterMatch[1].toUpperCase();
// 也尝试提取内容
const restContent = answer.replace(/^[A-Fa-f][..、|]?\s*/, '').trim();
if (restContent && restContent.length > 0 && !/^[A-Fa-f]$/.test(restContent)) {
answerContent = restContent;
log(`从答案提取: 字母=${answerLetters}, 内容="${answerContent}"`);
} else {
log(`从答案提取字母: ${answerLetters}`);
}
}
}
// 3. 【v5.87关键优化】优先使用内容匹配
// 因为选项顺序可能打乱,字母位置不可靠,文字内容才是最准确的
if (answerContent) {
log(`优先内容匹配: "${answerContent}"`);
const success = fillByContent(container, answerContent, questionType, positions);
if (success) {
log('✓ 内容匹配成功');
return true;
}
log('内容匹配失败,尝试字母匹配...');
}
// 4. 字母匹配(根据页面选项位置映射)
let clicked = 0;
const isMultiple = questionType === 'multiple';
const letters = answerLetters ? answerLetters.split('') : [];
if (letters.length > 0) {
log(`根据字母位置答题: ${letters.join('')}`);
for (const letter of letters) {
const pos = positions[letter];
if (pos) {
if (!pos.element.checked) {
pos.element.click();
log(`✓ 点击选项 ${letter} (第${pos.index + 1}个)`);
clicked++;
} else {
log(`✓ 选项 ${letter} 已选中`);
clicked++;
}
} else {
log(`⚠ 未找到字母 ${letter} 对应的选项位置`);
}
}
if (clicked > 0) {
log(`✓ 根据字母位置成功点击 ${clicked}/${letters.length} 个选项`);
return true;
}
}
// 5. 如果什么都没有,返回失败
log('无法匹配答案');
return false;
}
/**
* 根据选项内容匹配答题(备用方案)
*/
function fillByContent(container, answerContent, questionType, positions) {
const isMultiple = questionType === 'multiple';
// 多选题:支持 || 顿号、逗号分隔
let contentParts;
if (isMultiple) {
// 优先按 || 分隔,其次按顿号、逗号分隔
if (answerContent.includes('||')) {
contentParts = answerContent.split('||');
} else if (answerContent.includes('、')) {
// 【v5.87修复】支持顿号分隔(题库常用格式)
contentParts = answerContent.split(/[、]/);
} else {
contentParts = answerContent.split(/[,,]/);
}
} else {
contentParts = [answerContent];
}
let clicked = 0;
log(`内容匹配 (${isMultiple ? '多选' : '单选'}): ${contentParts.map(p => p.trim()).join(' | ')}`);
for (const content of contentParts) {
const contentClean = content.trim();
if (!contentClean) continue; // 跳过空内容
const contentNoPunct = contentClean.replace(/[。.!!??,,;;::]/g, '').trim();
let bestMatch = null;
let bestScore = 0;
for (const letter in positions) {
const pos = positions[letter];
const optContent = pos.content.trim();
const optNoPunct = optContent.replace(/[。.!!??,,;;::]/g, '').trim();
// 精确匹配
if (optContent === contentClean || optNoPunct === contentNoPunct) {
bestMatch = pos;
bestScore = 1;
break;
}
// 包含匹配
if (optNoPunct.includes(contentNoPunct) || contentNoPunct.includes(optNoPunct)) {
if (!bestMatch || bestScore < 0.9) {
bestMatch = pos;
bestScore = 0.9;
}
}
// 相似度匹配
if (optNoPunct.length >= 4 && contentNoPunct.length >= 4) {
let overlap = 0;
const minLen = Math.min(optNoPunct.length, contentNoPunct.length);
for (let i = 0; i < minLen - 1; i++) {
const sub = optNoPunct.substring(i, i + 2);
if (contentNoPunct.includes(sub)) overlap++;
}
const score = overlap / Math.max(optNoPunct.length, contentNoPunct.length);
if (score > bestScore && score >= 0.5) {
bestScore = score;
bestMatch = pos;
}
}
}
if (bestMatch) {
if (!bestMatch.element.checked) {
bestMatch.element.click();
log(`✓ 内容匹配点击 ${bestMatch.letter}: "${contentClean}" (相似度 ${(bestScore*100).toFixed(0)}%)`);
clicked++;
} else {
log(`✓ 内容匹配选项 ${bestMatch.letter} 已选中`);
clicked++;
}
} else {
log(`✗ 未找到匹配选项: "${contentClean}"`);
}
}
log(`内容匹配完成: 点击了 ${clicked} 个选项`);
return clicked > 0;
}
function fillBlankAnswer(container, answer) {
const input = container.querySelector('input[type="text"], input[type="number"], textarea');
if (!input) {
log('未找到填空题输入框');
return false;
}
const cleanAnswer = answer.trim();
input.value = cleanAnswer;
// 触发事件
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
log('✓ 填空题答案:', cleanAnswer);
return true;
}
/**
* 下拉选择框题型答题(案例阅读题等)
* 页面结构:
* 关键:value 值是固定映射:1=A, 2=B, 3=C, 4=D
* 选项顺序可能是打乱的(如 DCBA),但 value 值对应固定的字母
*/
function fillDropdownAnswer(container, answer) {
const select = container.querySelector('select');
if (!select) {
log('未找到下拉选择框');
return false;
}
// 解析答案字母
let answerLetter = answer.toUpperCase().trim();
// 如果是增强格式 "A|选项内容",只取字母
const enhancedMatch = answerLetter.match(/^([A-Fa-f])/);
if (enhancedMatch) {
answerLetter = enhancedMatch[1].toUpperCase();
}
log('下拉框答案字母:', answerLetter);
// 字母到 value 的映射(固定映射)
const letterToValue = {
'A': '1',
'B': '2',
'C': '3',
'D': '4',
'E': '5',
'F': '6'
};
const targetValue = letterToValue[answerLetter];
if (!targetValue) {
log('✗ 无法映射答案字母到 value:', answerLetter);
return false;
}
log('目标 value:', targetValue);
// 检查下拉框是否有这个 value
const options = select.querySelectorAll('option');
let hasValue = false;
options.forEach((opt, i) => {
log(` 选项${i}: value="${opt.value}" text="${opt.textContent?.trim()}"`);
if (opt.value === targetValue) {
hasValue = true;
}
});
if (!hasValue) {
log('✗ 下拉框中没有 value=' + targetValue + ' 的选项');
return false;
}
// 设置选中值
select.value = targetValue;
// 触发事件
select.dispatchEvent(new Event('change', { bubbles: true }));
select.dispatchEvent(new Event('input', { bubbles: true }));
log(`✓ 下拉框选择成功: 字母=${answerLetter} value=${targetValue}`);
return true;
}
// ==================== 答案查找 ====================
async function findAnswer(question, options) {
const bank = getBank();
// 清理题目文本
const questionClean = question.replace(/[_\s\u00A0]+/g, ' ').trim();
// 尝试多种匹配方式
// 1. 直接匹配
if (bank[questionClean]) {
log('✓ 题库直接匹配');
return bank[questionClean];
}
// 2. 去除标点匹配
const questionNoPunct = cleanText(questionClean);
for (let key in bank) {
if (cleanText(key) === questionNoPunct) {
log('✓ 题库去标点匹配');
return bank[key];
}
}
// 3. 填空题匹配(去除下划线)
const questionNoBlank = questionClean.replace(/[_\s\u00A0]+/g, '').trim();
for (let key in bank) {
const keyNoBlank = key.replace(/[_\s\u00A0]+/g, '').trim();
if (keyNoBlank === questionNoBlank) {
log('✓ 填空题匹配');
return bank[key];
}
}
// 4. 部分匹配
for (let key in bank) {
if (key.length > 20 && questionClean.includes(key.substring(0, key.length - 5))) {
log('✓ 题库部分匹配');
return bank[key];
}
}
log('题库未找到答案,尝试AI...');
return await callAI(questionClean, options);
}
// ==================== 分数提取 ====================
function extractScore() {
const bodyText = document.body.innerText;
// 尝试多种格式
let match = bodyText.match(/最高分[::]\s*(\d+\.?\d*)\s*\/\s*(\d+\.?\d*)/);
if (match) return { score: parseFloat(match[1]), total: parseFloat(match[2]) };
match = bodyText.match(/(\d+\.?\d*)\s*\/\s*(\d+\.?\d*)\s*(分|点)/);
if (match) return { score: parseFloat(match[1]), total: parseFloat(match[2]) };
match = bodyText.match(/(\d+\.?\d*)\s*分/);
if (match) return { score: parseFloat(match[1]), total: 100 };
return null;
}
// ==================== 提示框 ====================
function showTip(text, duration = 2000) {
let tip = document.getElementById('gk-tip');
if (!tip) {
tip = document.createElement('div');
tip.id = 'gk-tip';
tip.style.cssText = `
position: fixed; top: 10px; left: 50%; transform: translateX(-50%);
background: linear-gradient(135deg, #007bff, #0056b3);
color: white; padding: 12px 24px; border-radius: 8px;
font-size: 14px; z-index: 99999; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transition: opacity 0.3s;
`;
document.body.appendChild(tip);
}
tip.textContent = text;
tip.style.opacity = '1';
setTimeout(() => tip.style.opacity = '0', duration);
}
function showNotice(text) {
const notice = document.createElement('div');
notice.style.cssText = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: white; padding: 20px 30px; border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3); z-index: 99999;
white-space: pre-line; text-align: center; font-size: 14px;
border: 2px solid #007bff;
`;
notice.textContent = text;
document.body.appendChild(notice);
setTimeout(() => notice.remove(), 5000);
}
// ==================== 控制面板 ====================
function createPanel() {
log('创建面板...');
// 移除旧面板和旧快捷按钮
document.querySelector('#gk-panel')?.remove();
document.querySelector('#gk-toggle')?.remove();
// 获取面板状态
const panelHidden = getData('panelHidden', false);
log('面板状态:', panelHidden ? '隐藏' : '显示');
// 创建快捷按钮(始终显示)
const toggleBtn = document.createElement('div');
toggleBtn.id = 'gk-toggle';
toggleBtn.textContent = '📚';
toggleBtn.title = panelHidden ? '显示面板' : '隐藏面板';
toggleBtn.style.cssText = `
position: fixed; top: 10px; right: 10px;
width: 44px; height: 44px;
background: linear-gradient(135deg, #007bff, #0056b3);
color: white; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 22px; cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 999999; transition: transform 0.2s;
user-select: none;
`;
toggleBtn.onmouseenter = () => toggleBtn.style.transform = 'scale(1.1)';
toggleBtn.onmouseleave = () => toggleBtn.style.transform = 'scale(1)';
// 确保 body 存在后再添加
if (document.body) {
document.body.appendChild(toggleBtn);
log('✓ 快捷按钮已添加');
} else {
document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(toggleBtn);
log('✓ 快捷按钮已添加(延迟)');
});
}
// 创建主面板
const panel = document.createElement('div');
panel.id = 'gk-panel';
panel.innerHTML = `
📚 广开全自动助手 v5.87
题库: ${getBankCount()}题
`;
panel.style.cssText = `
position: fixed; top: 80px; right: 20px;
background: white; padding: 15px; border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2); z-index: 99998;
width: 220px; font-family: -apple-system, BlinkMacSystemFont, sans-serif;
transition: opacity 0.3s, transform 0.3s;
`;
// 根据状态设置面板显示
if (panelHidden) {
panel.style.opacity = '0';
panel.style.transform = 'translateX(100%)';
panel.style.pointerEvents = 'none';
}
// 确保 body 存在后再添加
if (document.body) {
document.body.appendChild(panel);
log('✓ 面板已添加');
} else {
document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(panel);
log('✓ 面板已添加(延迟)');
});
}
// 切换面板显示/隐藏
function togglePanel() {
const isHidden = getData('panelHidden', false);
const newHidden = !isHidden;
setData('panelHidden', newHidden);
if (newHidden) {
// 隐藏面板
panel.style.opacity = '0';
panel.style.transform = 'translateX(100%)';
panel.style.pointerEvents = 'none';
toggleBtn.title = '显示面板';
showTip('面板已隐藏,点击📚可显示');
} else {
// 显示面板
panel.style.opacity = '1';
panel.style.transform = 'translateX(0)';
panel.style.pointerEvents = 'auto';
toggleBtn.title = '隐藏面板';
}
}
// 快捷按钮点击事件
toggleBtn.onclick = togglePanel;
// 拖动功能
let isDragging = false, startX, startY, startRight, startTop;
panel.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX; startY = e.clientY;
startRight = parseInt(getComputedStyle(panel).right);
startTop = parseInt(getComputedStyle(panel).top);
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panel.style.right = (startRight - (e.clientX - startX)) + 'px';
panel.style.top = (startTop + (e.clientY - startY)) + 'px';
});
document.addEventListener('mouseup', () => isDragging = false);
// 按钮事件
document.getElementById('gk-start').onclick = () => {
setData('running', true);
showTip('开始运行...');
location.reload();
};
document.getElementById('gk-stop').onclick = () => {
setData('running', false);
showTip('已停止');
};
document.getElementById('gk-export').onclick = () => {
const bank = getBank();
const json = JSON.stringify(bank, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '广开题库_' + new Date().toISOString().slice(0,10) + '.json';
a.click();
URL.revokeObjectURL(url);
showTip('导出成功');
};
document.getElementById('gk-import').onclick = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const result = importBank(ev.target.result);
showNotice(result.success ?
`✅ 导入成功\n新增: ${result.added}题\n更新: ${result.updated}题\n总计: ${result.total}题` :
`❌ 导入失败: ${result.error}`);
createPanel();
};
reader.readAsText(file);
};
input.click();
};
document.getElementById('gk-clear').onclick = () => {
if (confirm('确定要清空题库吗?此操作不可恢复!\n请输入"确认清空"继续。')) {
const input = prompt('请输入"确认清空"');
if (input === '确认清空') {
saveBank({});
showTip('题库已清空');
createPanel();
}
}
};
document.getElementById('gk-token').onclick = () => {
const current = getData('cozeToken', '');
const newToken = prompt('请输入Coze API Token:', current);
if (newToken !== null) {
setData('cozeToken', newToken);
showTip('Token已保存');
}
};
document.getElementById('gk-close').onclick = togglePanel;
}
// ==================== 页面处理 ====================
async function handleMyPage() {
log('首页 - 扫描课程');
await wait(2000);
const progress = getProgress();
const courseLinks = [...document.querySelectorAll('a[href*="/course/user.php"]')];
for (const link of courseLinks) {
const courseId = new URL(link.href).searchParams.get('id');
if (courseId && !progress.finishedCourses.includes(courseId)) {
log(`进入课程: ${link.textContent.trim()}`);
showTip(`进入课程: ${link.textContent.trim().substring(0, 20)}`);
await wait(CONFIG.delay);
link.click();
return;
}
}
showNotice('🎉 所有课程已完成!');
setData('running', false);
createPanel();
}
async function handleCourseUserPage() {
log('形成性考核页面 - 扫描任务');
await wait(2000);
const progress = getProgress();
const courseId = getPageParam('id');
// 找所有测验活动容器
const activities = document.querySelectorAll('li.activity.quiz, li.activity[data-id]');
log(`找到 ${activities.length} 个活动`);
for (const activity of activities) {
// 获取链接
const link = activity.querySelector('a.aalink[href*="quiz/view.php"]');
if (!link) continue;
// 获取活动名称
const nameSpan = link.querySelector('.instancename');
const name = nameSpan ? nameSpan.textContent.replace('测验', '').trim() : link.textContent.trim();
// 跳过讨论
if (name.includes('讨论')) continue;
// 获取状态
const statusBtn = activity.querySelector('.activity-completion button, .completion-dropdown button');
const status = statusBtn ? statusBtn.textContent.trim() : '';
log(`${name}: ${status}`);
if (status === '待完成' || status === '未完成') {
log(`进入: ${name}`);
showTip(`进入: ${name}`);
await wait(CONFIG.delay);
link.click();
return;
}
}
log('所有形考已完成');
progress.finishedCourses.push(courseId);
saveProgress(progress);
showTip('课程完成');
await wait(CONFIG.delay);
location.href = '/my/';
}
async function handleLearningContentPage() {
log('学习内容页面 - 自动浏览资源');
await wait(2000);
// 找所有活动容器(资源、书籍等,排除讨论)
const activities = document.querySelectorAll('li.activity[data-id]');
log(`找到 ${activities.length} 个活动`);
for (const activity of activities) {
// 获取链接(资源、书籍等)
const link = activity.querySelector('a.aalink[href*="/mod/"]');
if (!link) continue;
// 获取活动名称
const nameSpan = link.querySelector('.instancename');
const name = nameSpan ? nameSpan.textContent.trim() : link.textContent.trim();
// 跳过讨论
if (name.includes('讨论') || link.href.includes('/forum/')) continue;
// 获取状态
const statusBtn = activity.querySelector('.activity-completion button, .completion-dropdown button');
const status = statusBtn ? statusBtn.textContent.trim() : '';
log(`${name}: ${status}`);
if (status === '待完成' || status === '未完成') {
log(`打开资源: ${name}`);
showTip(`打开资源: ${name}`);
await wait(CONFIG.delay);
// 打开链接
link.click();
// 等待页面加载并记录查看
await wait(5000);
// 返回上一页继续
history.back();
await wait(2000);
// 重新检测页面
return;
}
}
log('所有学习内容已浏览完成');
showTip('学习内容完成');
await wait(CONFIG.delay);
location.href = '/my/';
}
async function handleQuizViewPage() {
log('形考入口页');
await wait(1500);
// 先检查是否有确认弹窗(开始试答确认)
const confirmBtn = await checkAndHandleStartConfirm();
if (confirmBtn) {
return; // 已处理弹窗,等待页面跳转
}
// 查找"尝试测验"按钮
const allBtns = document.querySelectorAll('input[type="submit"], input[type="button"], button, a');
let startBtn = null;
for (const btn of allBtns) {
const text = (btn.value || btn.textContent || btn.innerText || '').trim();
if (text.includes('尝试测验') || text.includes('开始') || text.includes('继续')) {
startBtn = btn;
break;
}
}
if (startBtn) {
showTip('开始答题');
await wait(1000);
startBtn.click();
// 点击后等待一下,检查是否弹出确认框
await wait(1500);
await checkAndHandleStartConfirm();
} else {
showTip('形考已完成或无入口');
await wait(1000);
history.back();
}
}
/**
* 检查并处理"开始试答"确认弹窗
* 返回 true 表示处理了弹窗
*/
async function checkAndHandleStartConfirm() {
log('检查是否有确认弹窗...');
// 多种弹窗选择器
const modalSelectors = [
'.modal',
'[role="dialog"]',
'.moodle-dialogue',
'.yui-dialog',
'.modal-dialog',
'.modal-content',
'.dialogue',
'[data-region="modal"]',
'div[aria-modal="true"]',
'div.modal.show',
'div.modal.in'
];
for (const selector of modalSelectors) {
const modals = document.querySelectorAll(selector);
log(`选择器 ${selector}: 找到 ${modals.length} 个元素`);
for (const modal of modals) {
// 检查弹窗是否可见
const style = window.getComputedStyle(modal);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
continue;
}
// 检查弹窗内容
const modalText = modal.textContent || '';
log(`弹窗内容预览: ${modalText.substring(0, 50)}...`);
if (modalText.includes('开始试答') || modalText.includes('时间限制') || modalText.includes('试答限时')) {
log('✓ 发现"开始试答"确认弹窗');
// 查找弹窗中的所有按钮
const btns = modal.querySelectorAll('button, input[type="button"], input[type="submit"], a.btn, .btn, [role="button"]');
log(`弹窗中找到 ${btns.length} 个按钮`);
for (const btn of btns) {
const btnText = (btn.textContent || btn.value || '').trim();
log(` 按钮文本: "${btnText}"`);
if (btnText.includes('开始试答') || btnText === '开始试答') {
log('✓ 点击"开始试答"按钮');
showTip('确认开始答题');
await wait(500);
btn.click();
return true;
}
}
}
}
}
// 直接搜索包含"开始试答"文本的按钮
const allBtns = document.querySelectorAll('button, input[type="button"], input[type="submit"], a, .btn');
for (const btn of allBtns) {
const btnText = (btn.textContent || btn.value || '').trim();
if (btnText === '开始试答') {
log('✓ 直接找到"开始试答"按钮');
showTip('确认开始答题');
await wait(500);
btn.click();
return true;
}
}
log('未发现确认弹窗');
return false;
}
async function handleQuizResultPage() {
log('考核结果页');
await wait(2000);
const scoreInfo = extractScore();
if (!scoreInfo) {
showTip('无法提取分数');
await wait(CONFIG.delay);
history.back();
return;
}
const { score, total } = scoreInfo;
const percent = (score / total) * 100;
showTip(`分数: ${score}/${total} (${percent.toFixed(0)}%)`);
if (percent < CONFIG.targetScore) {
log(`分数低于${CONFIG.targetScore}%,重新试答`);
showTip(`分数低于${CONFIG.targetScore}%,重新答题`);
await wait(CONFIG.delay);
const allBtns = document.querySelectorAll('input[type="submit"], input[type="button"], button, a');
for (const btn of allBtns) {
const text = btn.value || btn.textContent || btn.innerText || '';
if (text.includes('重新试答') && !text.includes('回顾')) {
btn.click();
return;
}
}
history.back();
} else {
log(`分数达标: ${percent.toFixed(1)}%`);
const quizId = getPageParam('id');
const progress = getProgress();
if (quizId && !progress.finishedQuizzes.includes(quizId)) {
progress.finishedQuizzes.push(quizId);
saveProgress(progress);
}
await wait(CONFIG.delay);
// 先查找下一个形考链接(不包含"回顾"字样)
const allLinks = document.querySelectorAll('a');
for (const link of allLinks) {
const text = (link.textContent || link.innerText || '').trim();
// 匹配"考核一→"或"任务X→"等,但排除"回顾"相关链接
if ((text.match(/考核[一二三四五六七八九十\d]+[→▶>]/) || text.match(/任务\d+[→▶>]/)) && link.href.includes('quiz') && !text.includes('回顾')) {
showTip('进入下一个形考');
await wait(1000);
link.click();
return;
}
}
// 没有下一个形考,返回形成性考核列表页面
// 尝试多种面包屑选择器
const breadcrumbSelectors = '.breadcrumb a, nav[aria-label*="导航"] a, .navbar a, ol li a, .breadcrumb li a, nav.breadcrumb a, [role="navigation"] a, .path a';
const breadcrumbLinks = document.querySelectorAll(breadcrumbSelectors);
log('面包屑链接数量:', breadcrumbLinks.length);
for (const link of breadcrumbLinks) {
const text = (link.textContent || link.innerText || '').trim();
log('检查面包屑:', text);
if (text === '形成性考核') {
showTip('返回形成性考核页面');
await wait(1000);
link.click();
return;
}
}
// 面包屑没找到,在所有链接中精确匹配"形成性考核"(排除带"第X次"的)
for (const link of allLinks) {
const text = (link.textContent || link.innerText || '').trim();
// 精确匹配"形成性考核",排除"第一次形成性考核"等
if (text === '形成性考核') {
showTip('返回形成性考核页面');
await wait(1000);
link.click();
return;
}
}
// 最后尝试:从页面所有元素中查找包含"形成性考核"的可点击元素
const allElements = document.querySelectorAll('span, div, li');
for (const el of allElements) {
const text = (el.textContent || el.innerText || '').trim();
if (text === '形成性考核') {
// 检查是否有父级链接
const parentLink = el.closest('a');
if (parentLink) {
showTip('返回形成性考核页面');
await wait(1000);
parentLink.click();
return;
}
// 检查元素本身是否可点击
if (el.onclick || el.style.cursor === 'pointer') {
showTip('返回形成性考核页面');
await wait(1000);
el.click();
return;
}
}
}
// 没找到就返回上一页
showTip('返回课程页面');
await wait(1000);
history.back();
}
}
async function handleQuizAttemptPage() {
log('答题页面');
await wait(1500);
// 先处理案例阅读题(下拉选择框题型)
// 一个案例可能有多个子题,每个子题有独立的下拉框
const allSelects = document.querySelectorAll('.que select');
if (allSelects.length > 0) {
log(`发现 ${allSelects.length} 个下拉选择框,开始处理案例阅读题...`);
await handleDropdownQuestions();
}
// 再处理普通题目
const questions = document.querySelectorAll('.que');
let answered = 0, unmatched = 0;
for (const container of questions) {
if (!getData('running', false)) return;
// 跳过下拉选择框题型(已在上面处理)
if (container.querySelector('select')) {
continue;
}
const qtextEl = container.querySelector('.qtext');
if (!qtextEl) continue;
let questionText = qtextEl.innerText.trim().replace(/^试题\s*\d+\s*/g, '').trim();
// 判断题型
let questionType = 'single';
if (container.querySelectorAll('input[type="checkbox"]').length > 0) questionType = 'multiple';
else if (container.querySelectorAll('input[type="radio"]').length === 2) questionType = 'judge';
// 填空题:有文本输入框且没有单选/多选
else if (container.querySelectorAll('input[type="text"], input[type="number"], textarea').length > 0 &&
container.querySelectorAll('input[type="radio"], input[type="checkbox"]').length === 0) {
questionType = 'fill';
}
const options = [...container.querySelectorAll('.answer label, .r0 label, .r1 label, [data-region="answer-label"], .r0 .flex-fill, .r1 .flex-fill')]
.map(el => el.innerText.trim()).filter(Boolean);
log('处理题目:', questionText.substring(0, 30), '类型:', questionType);
const answer = await findAnswer(questionText, options);
if (answer && fillAnswer(container, answer, questionType)) {
answered++;
} else {
unmatched++;
}
await wait(CONFIG.delayBetweenQuestions);
}
log(`答题完成: ${answered}/${questions.length}, 未匹配: ${unmatched}`);
showTip(`答题完成: ${answered}/${questions.length}`);
await wait(1000);
// 点击下一页或提交
const nextBtn = document.querySelector('input[type="submit"][name="next"]');
if (nextBtn) {
nextBtn.click();
return;
}
const finishBtn = document.querySelector('input[type="submit"][name="finishattempt"], input[value*="结束试答"], input[value*="结束答题"]');
if (finishBtn) {
showTip('结束试答');
await wait(2000);
finishBtn.click();
}
}
/**
* 处理案例阅读题(下拉选择框题型)
* 一个案例包含多个子题,每个子题有独立的下拉选择框
* 【v5.87优化】增强题目匹配逻辑
*/
async function handleDropdownQuestions() {
const bank = getBank();
// 获取所有案例题目容器
const caseContainers = document.querySelectorAll('.que');
for (const container of caseContainers) {
// 检查是否包含下拉选择框
const selects = container.querySelectorAll('select');
if (selects.length === 0) continue;
log(`发现案例题,包含 ${selects.length} 个下拉选择框`);
// 获取案例正文(.qtext 中的案例描述)
const qtextEl = container.querySelector('.qtext');
const caseText = qtextEl ? qtextEl.innerText.trim() : '';
log(`案例正文: ${caseText.substring(0, 50)}...`);
// 遍历每个下拉选择框
for (let i = 0; i < selects.length; i++) {
const select = selects[i];
// 【v5.87优化】获取子题文本
let subQuestionText = '';
let questionKey = '';
// 方法1:从select的父容器中查找题目文本
const parent = select.closest('.content') || select.parentElement;
if (parent) {
// 查找题目编号和文本
const allText = parent.innerText || '';
const lines = allText.split('\n');
for (const line of lines) {
const trimmed = line.trim();
// 【v5.87修复】多种题目格式匹配
// 格式1: "数字、题目文本()"
// 格式2: "题目文本()" (无数字前缀)
// 格式3: 包含"答案"标记前的题目文本
if (trimmed.match(/^\d+[、..]/) || trimmed.match(/[((][))]/)) {
// 如果包含"答案"字样,提取前面的题目部分
if (trimmed.includes('答案')) {
const answerIdx = trimmed.indexOf('答案');
subQuestionText = trimmed.substring(0, answerIdx).trim();
} else {
subQuestionText = trimmed;
}
break;
}
}
// 如果没找到,尝试从前面的文本中查找
if (!subQuestionText) {
// 查找select前面的文本元素
let prevEl = select.previousElementSibling;
while (prevEl && !subQuestionText) {
const prevText = prevEl.innerText || prevEl.textContent || '';
if (prevText.match(/[((][))]/) && !prevText.includes('选择')) {
subQuestionText = prevText.trim();
}
prevEl = prevEl.previousElementSibling;
}
}
}
// 提取题目的核心部分(到括号为止)
if (subQuestionText) {
// 【v5.89修复】匹配完整的括号
// 格式: "1、题目文本()。答案" 或 "1、题目文本()"
// 优先匹配完整括号
let bracketMatch = subQuestionText.match(/^\d+[、..]\s*(.+?())/);
if (!bracketMatch) {
bracketMatch = subQuestionText.match(/^\d+[、..]\s*(.+?\(\))/);
}
if (bracketMatch) {
questionKey = bracketMatch[1].trim();
} else {
// 备用:匹配到左括号,然后补充右括号
const leftBracketMatch = subQuestionText.match(/^\d+[、..]\s*(.+?[((])/);
if (leftBracketMatch) {
questionKey = leftBracketMatch[1].trim();
} else {
// 最后备用:去掉编号
questionKey = subQuestionText.replace(/^\d+[、..]\s*/, '').trim();
}
}
// 清理选项文本(去掉选项A、B、C、D等)
questionKey = questionKey.replace(/[A-Fa-f][、..,,].*$/g, '').trim();
}
log(`子题 ${i + 1}: "${questionKey.substring(0, 40)}..."`);
// 查找答案
let answer = null;
// 【v5.87优化】多种匹配方式
// 1. 用题目的核心部分直接匹配
if (questionKey && bank[questionKey]) {
answer = bank[questionKey];
log('✓ 题库直接匹配');
}
// 2. 清理后匹配(去除标点符号)
if (!answer && questionKey) {
const cleanKey = questionKey.replace(/[,。、;:""''!?()\s]/g, '');
for (let key in bank) {
const cleanBankKey = key.replace(/[,。、;:""''!?()\s]/g, '');
if (cleanKey === cleanBankKey) {
answer = bank[key];
log('✓ 清理后匹配');
break;
}
}
}
// 3. 部分匹配(题目包含题库中的key,或题库key包含题目)
if (!answer && questionKey) {
for (let key in bank) {
// 双向部分匹配
if (key.length > 5 && (
questionKey.includes(key.substring(0, Math.min(key.length, 15))) ||
key.includes(questionKey.substring(0, Math.min(questionKey.length, 15)))
)) {
answer = bank[key];
log('✓ 题库部分匹配');
break;
}
}
}
// 4. 用案例前缀+序号匹配(备用格式)
if (!answer) {
const casePrefix = caseText.substring(0, 30);
const altKey = `${casePrefix}... 第${i + 1}小题`;
if (bank[altKey]) {
answer = bank[altKey];
log('✓ 备用格式匹配');
}
}
// 5. AI答题
if (!answer) {
log('题库未找到答案,尝试AI...');
const simpleQuestion = subQuestionText.length > 100 ?
subQuestionText.substring(0, 100) + '...' : subQuestionText;
answer = await callAI(simpleQuestion, null);
}
if (answer) {
// 填写答案
fillDropdownAnswerByElement(select, answer);
} else {
log('✗ 未找到答案');
}
await wait(300);
}
}
}
/**
* 直接通过 select 元素填写下拉框答案
* 【v5.87优化】优先匹配文字内容,再匹配字母
* 答案格式可能是:
* - "C.愤怒" -> 找到包含"愤怒"的选项
* - "C" -> 直接用字母映射(备用)
*/
function fillDropdownAnswerByElement(select, answer) {
log('下拉框答案原始:', answer);
// 【优先】提取答案文字内容进行匹配
// 格式: "C.愤怒" 或 "C、愤怒" 或 "愤怒"
let answerContent = '';
const contentMatch = answer.match(/[..、,,]\s*(.+)$/);
if (contentMatch) {
answerContent = contentMatch[1].trim();
log('提取答案文字:', answerContent);
}
// 如果有文字内容,优先在选项中匹配
if (answerContent) {
const options = select.querySelectorAll('option');
for (const opt of options) {
const optText = opt.textContent?.trim() || '';
// 检查选项文本是否包含答案文字
// 格式可能是 "C.愤怒" 或 "愤怒" 或 "C、愤怒"
// 提取选项的文字部分
const optContentMatch = optText.match(/[..、,,]\s*(.+)$/);
const optContent = optContentMatch ? optContentMatch[1].trim() : optText;
log(`检查选项: value="${opt.value}" text="${optText}" content="${optContent}"`);
// 文字内容匹配
if (optContent.includes(answerContent) || answerContent.includes(optContent)) {
log(`✓ 文字匹配成功: "${answerContent}" -> "${optText}"`);
select.value = opt.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
select.dispatchEvent(new Event('input', { bubbles: true }));
log(`✓ 下拉框选择成功: value=${opt.value}`);
return true;
}
}
log('文字匹配失败,尝试字母匹配...');
}
// 【备用】字母匹配逻辑
let answerLetter = answer.toUpperCase().trim();
// 如果答案包含字母和内容,只取字母
const letterMatch = answerLetter.match(/^([A-Fa-f])/);
if (letterMatch) {
answerLetter = letterMatch[1].toUpperCase();
}
log('下拉框答案字母:', answerLetter);
// 先尝试在下拉框中找到包含该字母的选项文本
const options = select.querySelectorAll('option');
for (const opt of options) {
const optText = opt.textContent?.trim() || '';
// 检查选项是否以该字母开头
if (optText.startsWith(answerLetter + '.') ||
optText.startsWith(answerLetter + '、') ||
optText.startsWith(answerLetter + '.') ||
optText === answerLetter) {
log(`✓ 字母文本匹配: "${answerLetter}" -> "${optText}"`);
select.value = opt.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
select.dispatchEvent(new Event('input', { bubbles: true }));
log(`✓ 下拉框选择成功: value=${opt.value}`);
return true;
}
}
// 最后使用固定映射(A→1, B→2, C→3, D→4)
const letterToValue = {
'A': '1',
'B': '2',
'C': '3',
'D': '4',
'E': '5',
'F': '6'
};
const targetValue = letterToValue[answerLetter];
if (targetValue) {
select.value = targetValue;
select.dispatchEvent(new Event('change', { bubbles: true }));
select.dispatchEvent(new Event('input', { bubbles: true }));
log(`✓ 下拉框选择(固定映射): 字母=${answerLetter} value=${targetValue}`);
return true;
}
log('✗ 无法匹配答案:', answer);
return false;
}
async function handleQuizSummaryPage() {
log('试答概要页面');
await wait(2000);
if (!getData('running', false)) return;
const allBtns = document.querySelectorAll('input, button');
for (const btn of allBtns) {
const text = btn.value || btn.textContent || btn.innerText || '';
if (text.includes('全部提交并结束') || text.includes('提交并结束')) {
showTip('提交...');
await wait(1500);
btn.click();
await wait(3000);
// 点击确认按钮
const allBtns2 = document.querySelectorAll('input, button, a, span.btn, div[role="button"]');
for (const btn2 of allBtns2) {
const text2 = btn2.value || btn2.textContent || btn2.innerText || '';
if (text2.includes('全部提交并结束') && btn2 !== btn) {
await wait(1000);
btn2.click();
return;
}
}
return;
}
}
}
async function handleQuizReviewPage() {
log('答题回顾 - 收集答案');
await wait(2000);
// 先检查是否有"在一页上显示所有试题"链接
const allLinks = document.querySelectorAll('a');
for (const link of allLinks) {
const text = (link.textContent || link.innerText || '').trim();
if (text === '在一页上显示所有试题') {
log('点击"在一页上显示所有试题"');
showTip('展开所有试题...');
link.click();
await wait(3000); // 等待页面加载
break;
}
}
const bank = getBank();
let added = 0, updated = 0, skipped = 0, failed = 0;
// 直接从DOM解析题目容器
const questionContainers = document.querySelectorAll('.que');
log(`找到 ${questionContainers.length} 个题目容器`);
questionContainers.forEach((container, idx) => {
// 1. 获取题目文本
const qtextEl = container.querySelector('.qtext');
if (!qtextEl) {
failed++;
return;
}
let question = qtextEl.innerText.trim();
// 清理多余空白
question = question.replace(/\s+/g, ' ').trim();
if (question.length < 5) {
failed++;
return;
}
// 2. 获取正确答案
const rightAnswerEl = container.querySelector('.rightanswer');
if (!rightAnswerEl) {
failed++;
log(`⚠ 未找到答案 #${idx + 1}: ${question.substring(0,30)}...`);
return;
}
let answer = rightAnswerEl.innerText.trim();
// 提取答案内容(去掉"正确答案是:"前缀,支持多种格式)
answer = answer.replace(/^正确答案(?:是)?[::]?[""'""]?\s*/, '').replace(/[""'""]?[。.]?$/, '').trim();
// 3. 判断题型
const isJudge = container.classList.contains('truefalse');
const isMultiple = container.querySelector('input[type="checkbox"]') !== null;
const isFill = container.querySelector('input[type="text"], input[type="number"], textarea') !== null &&
container.querySelectorAll('input[type="radio"], input[type="checkbox"]').length === 0;
const isDropdown = container.querySelector('select') !== null;
// 填空题直接保存文字答案
if (isFill) {
log(`✓ 填空题 ${idx + 1}: "${question.substring(0,30)}..." → ${answer}`);
if (!bank[question]) {
bank[question] = answer;
added++;
} else if (bank[question] !== answer) {
log(` 🔄 更新: "${bank[question]}" → "${answer}"`);
bank[question] = answer;
updated++;
} else {
skipped++;
}
return; // 继续下一个题目
}
// 下拉选择框题型:答案格式是 "题目文本 → 答案字母" 或 "题目文本 → 字母.内容"
// 【v5.87优化】保存完整答案格式(包含文字内容),答题时优先匹配文字
if (isDropdown) {
log(`下拉选择框题 ${idx + 1},解析答案文本...`);
log(`原始答案文本: ${answer.substring(0, 150)}...`);
// 【v5.87优化】解析答案格式
// 格式1: "题目文本()。A、选项... D、选项 → A, 题目2 → D"
// 格式2: "题目文本 → C.愤怒"(包含选项文字)
// 方法1:按 "→" 分割解析
const answerPairs = answer.split(/→/);
log(`找到 ${answerPairs.length} 个答案片段`);
let subIdx = 0;
const parsedQA = [];
for (let i = 0; i < answerPairs.length - 1; i++) {
const currentPart = answerPairs[i].trim();
const nextPart = answerPairs[i + 1] ? answerPairs[i + 1].trim() : '';
// 【v5.87优化】提取答案(可能是单个字母,也可能是"字母.内容")
let answerValue = '';
// 匹配格式: "A, 题目..." 或 "A" 或 "A.内容" 或 "A、内容"
const fullAnswerMatch = nextPart.match(/^([A-Fa-f][..、,,]?.*?)(?:[,,\s\n]|$)/);
if (fullAnswerMatch) {
answerValue = fullAnswerMatch[1].trim();
}
if (answerValue) {
subIdx++;
// 提取题目文本
let questionText = currentPart;
// 尝试提取带编号的题目 (如 "1、根据对目标的理解...")
const numMatch = currentPart.match(/(\d+)[、..]\s*(.+?)$/);
if (numMatch) {
questionText = numMatch[2].trim();
}
// 清理题目文本,只保留到括号为止(核心部分)
const bracketMatch = questionText.match(/^(.+?[((])/);
if (bracketMatch) {
questionText = bracketMatch[1].trim();
}
// 如果题目太长,取最后一句
if (questionText.length > 80) {
const sentences = questionText.split(/[。!?]/);
questionText = sentences[sentences.length - 1].trim();
}
// 清理选项文本
questionText = questionText.replace(/[A-Fa-f][、..,,].*$/g, '').trim();
if (questionText.length > 5) {
parsedQA.push({ question: questionText, answer: answerValue });
log(` 子题 ${subIdx}: "${questionText.substring(0, 40)}..." → ${answerValue}`);
}
}
}
// 保存解析结果
for (const qa of parsedQA) {
if (!bank[qa.question]) {
bank[qa.question] = qa.answer;
added++;
} else if (bank[qa.question] !== qa.answer) {
log(` 🔄 更新: "${bank[qa.question]}" → "${qa.answer}"`);
bank[qa.question] = qa.answer;
updated++;
} else {
skipped++;
}
}
// 如果上面解析失败,尝试正则直接提取
if (subIdx === 0) {
log('尝试备用解析方式...');
// 提取所有 "→ 答案" 模式(答案可能包含文字)
const arrowAnswerMatches = answer.match(/→\s*([A-Fa-f][..、,,]?[^\s,,→]*)/g);
if (arrowAnswerMatches) {
log(`备用方式提取到 ${arrowAnswerMatches.length} 个答案`);
// 同时尝试提取题目
const questionPattern = /(\d+)[、..]\s*([^→]+?)\s*→\s*([A-Fa-f][..、,,]?[^\s,,→]*)/g;
let match;
let qIdx = 0;
while ((match = questionPattern.exec(answer)) !== null) {
qIdx++;
let qText = match[2].trim();
const aValue = match[3].trim();
// 清理题目文本
const bracketMatch = qText.match(/^(.+?[((])/);
if (bracketMatch) {
qText = bracketMatch[1].trim();
}
// 清理选项文本
qText = qText.replace(/[A-Fa-f][、..,,].*$/g, '').trim();
if (qText.length > 5) {
log(` 备用子题 ${qIdx}: "${qText.substring(0, 40)}..." → ${aValue}`);
if (!bank[qText]) {
bank[qText] = aValue;
added++;
} else if (bank[qText] !== aValue) {
log(` 🔄 更新: "${bank[qText]}" → "${aValue}"`);
bank[qText] = aValue;
updated++;
} else {
skipped++;
}
}
}
// 如果还是没有提取到题目,用序号保存答案
if (qIdx === 0 && arrowAnswerMatches.length > 0) {
log('使用序号方式保存答案');
for (let i = 0; i < arrowAnswerMatches.length; i++) {
const value = arrowAnswerMatches[i].replace(/→\s*/, '').trim();
const subQuestion = `${question.substring(0, 30)}... 第${i + 1}小题`;
log(` 序号子题 ${i + 1}: "${subQuestion}" → ${value}`);
if (!bank[subQuestion]) {
bank[subQuestion] = value;
added++;
}
}
}
}
}
return; // 继续下一个题目
}
// 4. 提取选项内容映射(从试题回顾页面的答题区域)
function extractOptionsFromReviewPage(container) {
const options = {};
// 从答题区域的选项中提取
const answerEls = container.querySelectorAll('.answer .r0, .answer .r1, .answer > div');
const letterMap = ['A', 'B', 'C', 'D', 'E', 'F'];
answerEls.forEach((el, i) => {
// 获取选项文本
let text = el.innerText || el.textContent || '';
text = text.trim();
// 提取字母前缀(如果有)
const letterMatch = text.match(/^([A-Fa-f])[..、\s]/);
const letter = letterMatch ? letterMatch[1].toUpperCase() : letterMap[i];
// 提取选项内容(去掉字母前缀)
const content = text.replace(/^[A-Fa-f][..、\s]+/, '').trim();
if (content.length > 0) {
options[letter] = content;
}
});
return options;
}
const pageOptions = extractOptionsFromReviewPage(container);
log(`页面选项映射: ${JSON.stringify(pageOptions)}`);
// 5. 处理答案格式,保存为增强格式 "字母|选项内容"
if (isJudge) {
// 判断题:提取"对"或"错"
if (answer.includes('对')) {
answer = '对';
} else if (answer.includes('错')) {
answer = '错';
}
} else if (isMultiple) {
// 多选题
const letterMatch = answer.match(/[A-Ea-e]/g);
if (letterMatch && letterMatch.length > 1) {
// 字母格式答案(如 ABCD)
const allLetters = answer.replace(/[^A-Ea-e]/g, '').toUpperCase();
// 从页面选项中提取对应内容
const contents = [];
for (const letter of allLetters) {
if (pageOptions[letter]) {
contents.push(pageOptions[letter]);
}
}
if (contents.length > 0) {
// 保存为增强格式:字母|选项内容1||选项内容2||...
answer = allLetters + '|' + contents.join('||');
log(`多选答案增强: ${answer}`);
} else {
answer = allLetters;
}
} else {
// 文字格式答案(逗号分隔),需要匹配选项内容
const answerParts = answer.split(/[,,]/).map(a => a.trim()).filter(a => a.length > 0);
if (answerParts.length > 1) {
answer = answerParts.join('||');
log(`多选文字答案: ${answer}`);
}
}
} else {
// 单选题:答案是字母,需要从页面选项提取对应内容
const letterMatch = answer.match(/^[A-Fa-f]$/);
if (letterMatch) {
const letter = answer.toUpperCase();
// 从页面选项中查找对应内容
if (pageOptions[letter]) {
// 保存为增强格式:字母|选项内容
answer = letter + '|' + pageOptions[letter];
log(`✓ 单选答案增强: ${answer}`);
} else {
answer = letter;
log(`⚠ 未找到页面选项 ${letter} 的内容`);
}
}
// 文字答案保持原样
}
log(`✓ 题目 ${idx + 1}: "${question.substring(0,30)}..." → ${answer}`);
// 6. 更新题库(总是用试题回顾的正确答案覆盖)
if (!bank[question]) {
bank[question] = answer;
added++;
} else if (bank[question] !== answer) {
log(` 🔄 更新: "${bank[question]}" → "${answer}"`);
bank[question] = answer;
updated++;
} else {
skipped++;
}
});
// 保存题库
if (added > 0 || updated > 0) {
saveBank(bank);
}
let message = `✅ 收集完成\n新增:${added}题\n更新:${updated}题\n跳过:${skipped}题\n失败:${failed}题\n总计:${Object.keys(bank).length}题`;
showNotice(message);
await wait(CONFIG.delay);
const allBtns = document.querySelectorAll('input[type="submit"], input[type="button"], button, a');
for (const btn of allBtns) {
const text = btn.value || btn.textContent || btn.innerText || '';
if (text.includes('结束回顾')) {
btn.click();
return;
}
}
history.back();
}
async function handleForumViewPage() {
log('讨论列表');
await wait(2000);
const progress = getProgress();
const forumId = getPageParam('id');
const rows = document.querySelectorAll('.forumheaderlist tbody tr, table tbody tr');
for (const row of rows) {
const link = row.querySelector('a[href*="discuss.php"]');
if (!link) continue;
const discussId = new URL(link.href).searchParams.get('d');
if (discussId && !progress.finishedDiscussions.includes(discussId)) {
showTip('进入讨论');
setData('forumViewUrl', location.href);
await wait(CONFIG.delay);
link.click();
return;
}
}
progress.finishedDiscussions.push(forumId);
saveProgress(progress);
await wait(CONFIG.delay);
history.back();
}
async function handleForumDiscussPage() {
log('讨论详情');
await wait(2000);
const replyBtn = document.querySelector('a[href*="post.php"][href*="reply"]');
if (!replyBtn) {
const discussId = getPageParam('d');
const progress = getProgress();
if (discussId && !progress.finishedDiscussions.includes(discussId)) {
progress.finishedDiscussions.push(discussId);
saveProgress(progress);
}
await wait(1000);
location.href = getData('forumViewUrl', '/my/');
return;
}
const posts = document.querySelectorAll('.forumpost .posting');
let replyContent = '学习了这门课程,收获很大。';
if (posts.length > 1) {
const lastText = posts[posts.length - 1].innerText.trim().substring(0, 80);
replyContent = `感谢分享!关于"${lastText}...",我也有一些体会。`;
}
setData('replyContent', replyContent);
showTip('准备回复');
await wait(CONFIG.delay);
replyBtn.click();
}
async function handleForumPostPage() {
log('回复页面');
await wait(2000);
const textarea = document.querySelector('textarea[name="message"], #id_message, .editor-textarea');
if (textarea) {
textarea.value = getData('replyContent', '学习了,收获很大。');
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
const submitBtn = document.querySelector('input[type="submit"][name="submit"], button[type="submit"]');
if (submitBtn) {
showTip('提交回复');
await wait(1000);
submitBtn.click();
const discussId = getPageParam('d') || getPageParam('reply');
const progress = getProgress();
if (discussId && !progress.finishedDiscussions.includes(discussId)) {
progress.finishedDiscussions.push(discussId);
saveProgress(progress);
}
}
}
// ==================== 主控制器 ====================
async function main() {
const pageType = detectPageType();
log('页面类型:', pageType);
// 始终创建面板和快捷按钮(优先显示)
createPanel();
if (!getData('running', false)) {
return;
}
// 等待面板渲染完成
await wait(500);
switch (pageType) {
case PageType.MY: await handleMyPage(); break;
case PageType.COURSE_USER: await handleCourseUserPage(); break;
case PageType.LEARNING_CONTENT: await handleLearningContentPage(); break;
case PageType.QUIZ_VIEW: await handleQuizViewPage(); break;
case PageType.QUIZ_RESULT: await handleQuizResultPage(); break;
case PageType.QUIZ_ATTEMPT: await handleQuizAttemptPage(); break;
case PageType.QUIZ_SUMMARY: await handleQuizSummaryPage(); break;
case PageType.QUIZ_REVIEW: await handleQuizReviewPage(); break;
case PageType.FORUM_VIEW: await handleForumViewPage(); break;
case PageType.FORUM_DISCUSS: await handleForumDiscussPage(); break;
case PageType.FORUM_POST: await handleForumPostPage(); break;
default: createPanel();
}
}
// 启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})();