// ==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(); } })();