// ==UserScript== // @name 小雅答答答 // @license MIT // @version 2.9.9.1 // @description 小雅平台学习助手 📖,智能整理归纳学习资料 📚,辅助完成练习 💪,并提供便捷的查阅和修改功能 📝! // @author Yi // @match https://*.ai-augmented.com/* // @icon https://www.ai-augmented.com/static/logo3.1dbbea8f.png // @grant GM_xmlhttpRequest // @grant GM_info // @run-at document-start // @connect api.open.uc.cn // @connect page-souti.myquark.cn // @connect api.qrserver.com // @connect ai-augmented.com // @connect g.alicdn.com // @require https://cdn.jsdmirror.com/npm/katex@0.16.9/dist/katex.min.js // @require https://cdn.jsdmirror.com/npm/docx@7.1.0/build/index.min.js // @require https://cdn.jsdmirror.com/npm/file-saver@2.0.5/dist/FileSaver.min.js // @require https://cdn.jsdmirror.com/npm/js-md5@0.8.3/src/md5.min.js // @require https://cdn.jsdmirror.com/npm/crypto-js@4.2.0/crypto-js.js // @require https://cdn.jsdmirror.com/npm/crypto-js@4.2.0/hmac-sha1.js // @require https://cdn.jsdmirror.com/npm/dom-to-image-more@3.2.0/dist/dom-to-image-more.min.js // @require https://cdn.jsdmirror.com/npm/katex@0.16.9/dist/contrib/auto-render.min.js // @homepageURL https://xiaoya.zygame1314.site // ==/UserScript== (function () { 'use strict'; const RuntimePatcher = { _nativeRefs: { dispatchEvent: window.dispatchEvent, removeItem: Storage.prototype.removeItem, register: navigator.serviceWorker.register, fetch: window.fetch, xhrOpen: XMLHttpRequest.prototype.open, xhrSend: XMLHttpRequest.prototype.send, sendBeacon: navigator.sendBeacon ? navigator.sendBeacon.bind(navigator) : null }, _decode: (str) => decodeURIComponent(escape(atob(str))), _allowRemovalOnce: false, blessedRemoveItem: function (storageInstance, key) { this._allowRemovalOnce = true; try { storageInstance.removeItem(key); } finally { this._allowRemovalOnce = false; } }, _clearStaleWorkers: function () { if ('serviceWorker' in navigator) { const swTargetName = this._decode('Z2xvYmFsLXNlcnZpY2Utd29ya2VyLmpz'); navigator.serviceWorker.getRegistrations().then(registrations => { for (let registration of registrations) { if (registration.active && registration.active.scriptURL.includes(swTargetName)) { registration.unregister().then(success => { if (success) { console.log('[运行时] 已注销过期的 service worker,正在刷新页面以生效。'); location.reload(); } }); } } }); } }, _applyEventShim: function () { const nativeDispatch = this._nativeRefs.dispatchEvent; const patchedDispatch = function (...args) { return nativeDispatch.apply(window, args); }; Object.defineProperty(patchedDispatch, 'toString', { value: () => 'function dispatchEvent() { [native code] }', configurable: true, }); window.dispatchEvent = patchedDispatch; }, _initSecureStorage: function () { const nativeRemoveItem = this._nativeRefs.removeItem; const protectedKeys = new Set([ 'xiaoya_access_token', 'xiaoya_refresh_token', 'xiaoya_bound_user_id', 'paperDescription', 'recordId', 'groupId', 'paperId', 'assignmentTitle', 'submittedAnswerData', 'answerData', 'aiCustomPrompts', 'aiConfig', 'quark_service_ticket', 'quark_ticket_expiry' ]); const self = this; Storage.prototype.removeItem = function (key) { if (self._allowRemovalOnce && protectedKeys.has(key)) { console.log(`[运行时] 脚本自身授权移除受保护的键: "${key}"`); return nativeRemoveItem.apply(this, arguments); } if (protectedKeys.has(key)) { console.log(`[反检测] 检测到对受保护键 "${key}" 的移除尝试,执行反检测策略。`); const value = this.getItem(key); nativeRemoveItem.apply(this, arguments); console.log(`[反检测] 已临时移除键 "${key}" 以绕过同步检测。`); if (value) { setTimeout(() => { this.setItem(key, value); console.log(`[反检测] 已恢复键 "${key}"。`); }, 0); } else { console.log(`[反检测] 键 "${key}" 原本就为空,无需恢复。`); } return; } return nativeRemoveItem.apply(this, arguments); }; Object.defineProperty(Storage.prototype.removeItem, 'toString', { value: () => 'function removeItem() { [native code] }', configurable: true, }); }, _manageWorkerLifecycle: function () { const nativeRegister = this._nativeRefs.register; const swTargetName = this._decode('Z2xvYmFsLXNlcnZpY2Utd29ya2VyLmpz'); navigator.serviceWorker.register = function (scriptURL, options) { if (typeof scriptURL === 'string' && scriptURL.includes(swTargetName)) { console.error(`[运行时] 阻止了受限的 service worker 注册:`, scriptURL); return Promise.reject(new DOMException('当前运行时策略不允许注册。', 'SecurityError')); } return nativeRegister.apply(navigator.serviceWorker, arguments); }; }, _setupUIMonitor: function () { const modalSignature = this._decode('5qGI5rWL5Yiw5q2j5Zyo5L2/55So5by655S15bel5YW3'); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.textContent.includes(modalSignature)) { const modalRoot = node.closest('div[style*="z-index"]'); if (modalRoot) { console.warn('[运行时] 检测到并移除了侵入式 UI 弹窗。'); modalRoot.remove(); } } } } }); const startObserver = () => { if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { setTimeout(startObserver, 50); } }; startObserver(); }, _initNetworkInterceptor: function () { const self = this; const blockedHostname = 'log.aliyuncs.com'; const SUBMIT_URL_SIGNATURE = '/api/jx-iresource/survey/submit'; const SUBMISSION_CONTRIBUTION_DELAY = 5000; async function triggerImmediateContribution(groupId, nodeId) { if (!autoContributeEnabled) { console.log('[自动贡献] 检测到作业提交,但自动贡献功能已关闭,跳过。'); return; } if (!groupId || !nodeId) { console.warn('[自动贡献] 无法从当前页面URL获取 groupId 或 nodeId,贡献中止。'); return; } const sessionKey = `submitted_and_contributed_${groupId}_${nodeId}`; if (sessionStorage.getItem(sessionKey)) { console.log(`[自动贡献] 作业 (G:${groupId}, N:${nodeId}) 在本次会话中已提交并贡献过,跳过重复触发。`); return; } const NOTIFICATION_ID = `contribution-after-submit-${groupId}-${nodeId}`; showNotification( '作业提交成功!请不要关闭页面,后台正在为你准备并贡献答案...', { type: 'info', duration: 0, id: NOTIFICATION_ID, animation: 'scale' } ); for (let i = SUBMISSION_CONTRIBUTION_DELAY / 1000; i > 0; i--) { await new Promise(resolve => setTimeout(resolve, 1000)); showNotification( `请勿关闭页面... 正在等待服务器批改 (剩余 ${i} 秒)`, { type: 'info', duration: 0, id: NOTIFICATION_ID } ); } showNotification('服务器已批改,正在贡献答案...', { type: 'info', duration: 0, id: NOTIFICATION_ID }); try { const result = await contributeSingleAssignment(groupId, nodeId); if (result.success) { showNotification(`✅ 答案已成功贡献。现在可以安全关闭页面了。`, { type: 'success', duration: 10000, id: NOTIFICATION_ID }); sessionStorage.setItem(sessionKey, 'true'); } else { showNotification(`⚠️ 答案贡献失败: ${result.error}。现在可以关闭页面了。`, { type: 'warning', duration: 10000, id: NOTIFICATION_ID }); console.warn(`[自动贡献] 提交后贡献失败: ${result.error}`); } } catch (error) { showNotification(`💥 答案贡献时发生严重错误。现在可以关闭页面了。`, { type: 'error', duration: 10000, id: NOTIFICATION_ID }); console.error(`[自动贡献] 提交后贡献时发生严重错误:`, error); } } window.fetch = function (input, init) { const nativeFetch = self._nativeRefs.fetch; let urlStr; if (typeof input === 'string') { urlStr = input; } else if (input instanceof Request) { urlStr = input.url; } else { urlStr = String(input); } try { const urlObj = new URL(urlStr, window.location.origin); if (urlObj.hostname.endsWith(blockedHostname)) { console.warn(`[运行时] 阻止了向日志服务器的 fetch 请求:`, urlStr); return Promise.resolve(new Response('{"success":true}', { status: 200, headers: { 'Content-Type': 'application/json' } })); } if (urlStr.includes(SUBMIT_URL_SIGNATURE) && init && (init.method || '').toUpperCase() === 'POST') { console.log('[自动贡献] 检测到作业提交请求 (fetch),将在其成功后触发贡献。'); const originalFetchPromise = nativeFetch.apply(window, arguments); originalFetchPromise.then(response => { const clonedResponse = response.clone(); if (clonedResponse.ok) { clonedResponse.json().then(data => { if (data.success) { const groupId = getGroupIDFromUrl(window.location.href); const nodeId = getNodeIDFromUrl(window.location.href); triggerImmediateContribution(groupId, nodeId); } }); } }).catch(err => { console.warn('[自动贡献] 作业提交请求 (fetch) 失败,不触发贡献。', err); }); return originalFetchPromise; } } catch (e) { console.warn('[运行时] 解析 fetch 目标 URL 时发生异常,回退至原生 fetch:', e); } return nativeFetch.apply(window, arguments); }; Object.defineProperty(window.fetch, 'toString', { value: () => 'function fetch() { [native code] }', configurable: true, }); XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._requestURL = url; return self._nativeRefs.xhrOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { if (this._requestURL) { try { const urlObj = new URL(this._requestURL, window.location.origin); if (urlObj.hostname.endsWith(blockedHostname)) { console.warn(`[运行时] 阻止了向日志服务器的 XMLHttpRequest 请求:`, this._requestURL); Object.defineProperties(this, { 'readyState': { value: 4, writable: true }, 'status': { value: 200, writable: true }, 'responseText': { value: '{"success":true}', writable: true }, 'response': { value: '{"success":true}', writable: true } }); this.dispatchEvent(new Event('readystatechange')); this.dispatchEvent(new Event('load')); return; } if (this._requestURL.includes(SUBMIT_URL_SIGNATURE)) { console.log('[自动贡献] 检测到作业提交请求 (XHR),将在其成功后触发贡献。'); this.addEventListener('load', async function () { if (this.status >= 200 && this.status < 300) { let responseText; if (this.responseType === '' || this.responseType === 'text') { responseText = this.responseText; } else if (this.responseType === 'blob') { try { responseText = await this.response.text(); } catch (blobError) { console.warn('[自动贡献] 读取Blob响应时出错', blobError); return; } } else { console.warn(`[自动贡献] 无法处理响应类型 '${this.responseType}',跳过。`); return; } try { const data = JSON.parse(responseText); if (data.success) { const groupId = getGroupIDFromUrl(window.location.href); const nodeId = getNodeIDFromUrl(window.location.href); triggerImmediateContribution(groupId, nodeId); } } catch (e) { console.warn('[自动贡献] 解析XHR提交响应失败,不触发贡献。', e); } } }, { once: true }); } } catch (e) { console.error('[运行时] 处理 XMLHttpRequest URL 时发生异常,回退至原始 send:', e); } } return self._nativeRefs.xhrSend.apply(this, arguments); }; if (self._nativeRefs.sendBeacon) { navigator.sendBeacon = function (url, data) { try { const urlObj = new URL(url, window.location.origin); if (urlObj.hostname.endsWith(blockedHostname)) { console.warn(`[运行时] 阻止了向日志服务器的 sendBeacon 请求:`, url); return true; } } catch (e) { console.warn('[运行时] sendBeacon URL 解析失败,回退至原生实现:', e); } try { return self._nativeRefs.sendBeacon.call(navigator, url, data); } catch (err) { console.error('[运行时] 调用原生 sendBeacon 失败:', err); return false; } }; } console.log('[运行时] 网络请求拦截器已部署 (含提交后自动贡献功能 - 增强通知版)。'); }, run: function () { console.log('[运行时] 正在初始化运行时补丁...'); this._clearStaleWorkers(); this._applyEventShim(); this._initSecureStorage(); this._manageWorkerLifecycle(); this._setupUIMonitor(); this._initNetworkInterceptor(); console.log('[运行时] 补丁已成功应用。'); } }; RuntimePatcher.run(); const KATEX_RENDER_OPTIONS = { delimiters: [ { left: '$$', right: '$$', display: true }, { left: '\\[', right: '\\]', display: true }, { left: '\\(', right: '\\)', display: false }, { left: '$', right: '$', display: false } ], throwOnError: false, trust: true, ignoredTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'], ignoredClasses: ['katex'] }; const MATH_CONTENT_REGEX = /(?:\$\$|\\\[|\\\(|\\begin\{|\\frac|\\sqrt|\\sum|\\int|\\alpha|\\beta|\\gamma|_{|\\mathrm|\\left|\\right|\\pi|\\theta)/; const LATEX_IMAGE_ENDPOINT = 'https://latex.codecogs.com/png.image?'; function applyMathRendering(rootElement) { if (!rootElement) return; if (typeof window.renderMathInElement !== 'function') return; try { window.renderMathInElement(rootElement, KATEX_RENDER_OPTIONS); } catch (error) { console.warn('[KaTeX] 渲染公式时出现问题:', error); } } const defaultPrompts = { '1': ` 你是一个用于解答单选题的 AI 助手。请根据以下题目和选项,选择唯一的正确答案。 【题目类型】: {questionType} 【题目内容】: {questionTitle} 【选项】: {optionsText} --- 【输出要求】: 1. 你的回答必须严格遵守以下格式:仅包含唯一正确选项的字母(例如:"A")。 2. 不要包含任何其他文字、解释、标点符号或空格。 3. 在回答结束后,必须在新的一行输出一个JSON对象来提供你的置信度评分(1-5),格式为:{"confidence": 5} 【示例】: A {"confidence": 5} `.trim(), '2': ` 你是一个用于解答多选题的 AI 助手。请根据以下题目和选项,选择所有正确的答案。 【题目类型】: {questionType} 【题目内容】: {questionTitle} 【选项】: {optionsText} --- 【输出要求】: 1. 你的回答必须严格遵守以下格式:仅包含所有正确选项的字母,并用英文逗号分隔(例如:"A,C")。 2. 不要包含任何其他文字、解释、标点符号或空格。 3. 在回答结束后,必须在新的一行输出一个JSON对象来提供你的置信度评分(1-5),格式为:{"confidence": 4} `.trim(), '5': ` 你是一个用于解答判断题的 AI 助手。请根据以下题目,判断其表述是否正确。 【题目类型】: {questionType} 【题目内容】: {questionTitle} 【选项】: A. 正确 B. 错误 --- 【输出要求】: 1. 你的回答必须严格遵守以下格式:仅包含唯一正确选项的字母(例如:"A")。 2. 不要包含任何其他文字、解释、标点符号或空格。 3. 在回答结束后,必须在新的一行输出一个JSON对象来提供你的置信度评分(1-5),格式为:{"confidence": 5} `.trim(), '4': ` 你是一个用于解答填空题的 AI 助手。请根据题目内容,为每一个空白处生成最合适的答案。 【题目类型】: {questionType} 【题目内容】: {questionTitle} --- 【输出要求】: 1. 你的回答必须是一个 JSON 数组,数组中的每个字符串元素按顺序对应题目中的每一个空白处。 2. 不要包含任何其他文字、解释、标点符号或空格。 3. 在 JSON 数组之后,必须在全新的一行输出另一个JSON对象来提供你的置信度评分(1-5),格式为:{"confidence": 3} 【示例】: ["答案一", "答案二"] {"confidence": 3} `.trim(), '6': ` 你是一位精通各大学科的答题助手。请根据以下【{questionType}】的题目要求,生成一份简洁、准确、专业的答案。 【题目】: {questionTitle} 【我已有的答案草稿】(可参考或忽略): {answerContent} --- 【生成要求】: 1. 直接输出纯文本,不要包含任何额外的解释文字或 Markdown 格式化标记。 2. 在所有答案内容结束后,必须在全新的一行输出一个JSON对象来提供你的置信度评分(1-5),格式为:{"confidence": 4} 3. 不要在评分后添加任何额外内容。 `.trim(), '10': ` 你是一位专业的编程助手。请根据以下【编程题】的要求,使用指定的编程语言生成完整的代码解决方案。 【题目描述】: {questionTitle} 【要求语言】: {language} 【时间限制】: {max_time} ms 【内存限制】: {max_memory} KB 【我已有的代码】(可参考或忽略): {answerContent} --- 【生成要求】: 1. 直接输出纯代码文本,不要包含任何额外的解释文字或 Markdown 格式化标记。 2. 在所有代码结束后,必须在全新的一行输出一个JSON对象来提供你的置信度评分(1-5),格式为:{"confidence": 5} 3. 不要在评分后添加任何额外内容。 `.trim(), '12': ` 你是一个用于解答排序题的 AI 助手。请根据题目要求,将给出的选项排列成正确的顺序。 【题目类型】: {questionType} 【题目内容】: {questionTitle} 【需要排序的选项】: {optionsText} --- 【输出要求】: 1. 你的回答必须是一个 JSON 数组,其中包含表示正确顺序的选项字母。例如:["C", "A", "B"] 2. 不要包含任何其他文字、解释、标点符号或空格。 3. 在 JSON 数组之后,必须在全新的一行输出另一个JSON对象来提供你的置信度评分(1-5),格式为:{"confidence": 5} `.trim(), '13': ` 你是一个用于解答匹配题的 AI 助手。请为左侧列表的每一项,从右侧列表中选择最合适的匹配项。 【题目类型】: {questionType} 【题目内容】: {questionTitle} 【左侧列表 (需要匹配的项)】: {stemsText} 【右侧列表 (可用的选项)】: {optionsText} --- 【输出要求】: 1. 你的回答必须是一个 JSON 对象。例如: {"A": "b", "B": "a", "C": "d"} 2. 不要包含任何其他文字、解释、标点符号或空格。 3. 在 JSON 对象之后,必须在全新的一行输出另一个JSON对象来提供你的置信度评分(1-5),格式为:{"confidence": 4} `.trim() }; const SCRIPT_CONFIG = { priorityApiBaseUrl: 'https://xiaoya-get-cdn.zygame1314.site', remoteConfigUrls: [ 'https://gist.githubusercontent.com/zygame1314/5e8a64928374c3fcc88a235f8f75d6e7/raw/xiaoya-config.json', 'https://gh-proxy.com/gist.githubusercontent.com/zygame1314/5e8a64928374c3fcc88a235f8f75d6e7/raw/xiaoya-config.json', 'https://ghfast.top/gist.githubusercontent.com/zygame1314/5e8a64928374c3fcc88a235f8f75d6e7/raw/xiaoya-config.json' ], defaultApiBaseUrl: 'https://xiaoya-manage.zygame1314-666.top', cachedApiBaseUrl: null, lastFetchTimestamp: 0, cacheDuration: 300000 }; const HealthCheckVisualizer = { container: null, groups: {}, _createContainer() { if (this.container) return; this.container = document.createElement('div'); this.container.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.75); padding: 12px 20px; border-radius: 20px; z-index: 100001; display: flex; flex-direction: column; align-items: center; gap: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.25); backdrop-filter: blur(6px); transition: opacity 0.4s ease, transform 0.4s ease; opacity: 0; transform: translateX(-50%) translateY(20px); font-family: Microsoft YaHei; `; document.body.appendChild(this.container); requestAnimationFrame(() => { this.container.style.opacity = '1'; this.container.style.transform = 'translateX(-50%) translateY(0)'; }); }, addGroup(groupId, label, urls, isPriority = false) { this._createContainer(); if (this.groups[groupId]) return; const groupDiv = document.createElement('div'); groupDiv.style.cssText = `display: flex; align-items: center; gap: 12px;`; const labelSpan = document.createElement('span'); labelSpan.textContent = label; labelSpan.style.color = '#fff'; labelSpan.style.fontSize = '13px'; labelSpan.style.fontWeight = 'bold'; groupDiv.appendChild(labelSpan); const dotsContainer = document.createElement('div'); dotsContainer.style.cssText = `display: flex; align-items: center; gap: 8px;`; groupDiv.appendChild(dotsContainer); const dots = urls.map(() => { const dot = document.createElement('div'); dot.style.cssText = ` width: ${isPriority ? '14px' : '12px'}; height: ${isPriority ? '14px' : '12px'}; border-radius: 50%; background-color: #9ca3af; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); ${isPriority ? 'border: 2px solid rgba(251, 191, 36, 0.5);' : ''} `; dotsContainer.appendChild(dot); return dot; }); this.container.appendChild(groupDiv); this.groups[groupId] = { groupDiv, labelSpan, dots }; }, updateDot(groupId, index, status) { if (!this.groups[groupId] || !this.groups[groupId].dots[index]) return; const group = this.groups[groupId]; const dot = group.dots[index]; dot.getAnimations().forEach(anim => anim.cancel()); const colors = { testing: '#f59e0b', success: '#22c55e', failure: '#ef4444' }; dot.style.backgroundColor = colors[status]; dot.style.transform = 'scale(1)'; switch (status) { case 'testing': group.labelSpan.textContent = group.labelSpan.textContent.replace('...', '中...'); dot.animate([ { transform: 'scale(1.0)', opacity: 0.7 }, { transform: 'scale(1.3)', opacity: 1 }, { transform: 'scale(1.0)', opacity: 0.7 } ], { duration: 1200, iterations: Infinity, easing: 'ease-in-out' }); break; case 'success': dot.animate([ { transform: 'scale(1.4)', backgroundColor: '#a7f3d0' }, { transform: 'scale(1)' } ], { duration: 400, easing: 'ease-out' }); break; case 'failure': dot.animate([ { transform: 'translateX(-3px)' }, { transform: 'translateX(3px)' }, { transform: 'translateX(-2px)' }, { transform: 'translateX(2px)' }, { transform: 'translateX(0)' } ], { duration: 300, easing: 'ease-in-out' }); break; } }, updateGroupLabel(groupId, newLabel) { if (this.groups[groupId]) { this.groups[groupId].labelSpan.textContent = newLabel; } }, destroy() { if (this.container) { this.container.style.opacity = '0'; this.container.style.transform = 'translateX(-50%) translateY(20px)'; setTimeout(() => { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } this.container = null; this.groups = {}; }, 400); } } }; const ContributionProgressUI = { ring: null, progressCircle: null, radius: 36, circumference: 0, container: null, mainBall: null, originalTitle: '', init(menuContainer) { if (this.ring) return; this.container = menuContainer; this.mainBall = menuContainer.querySelector('.xiaoya-main-ball'); this.originalTitle = this.mainBall.title || '小雅答答答'; this.circumference = 2 * Math.PI * this.radius; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '80'); svg.setAttribute('height', '80'); svg.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-90deg); z-index: -1; display: none; opacity: 0; transition: opacity 0.4s ease; `; this.ring = svg; const trackCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); trackCircle.setAttribute('cx', '40'); trackCircle.setAttribute('cy', '40'); trackCircle.setAttribute('r', this.radius); trackCircle.setAttribute('stroke', 'rgba(0, 0, 0, 0.1)'); trackCircle.setAttribute('stroke-width', '5'); trackCircle.setAttribute('fill', 'transparent'); svg.appendChild(trackCircle); this.progressCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); this.progressCircle.setAttribute('cx', '40'); this.progressCircle.setAttribute('cy', '40'); this.progressCircle.setAttribute('r', this.radius); this.progressCircle.setAttribute('stroke', '#4F46E5'); this.progressCircle.setAttribute('stroke-width', '5'); this.progressCircle.setAttribute('fill', 'transparent'); this.progressCircle.setAttribute('stroke-linecap', 'round'); this.progressCircle.style.strokeDasharray = `${this.circumference} ${this.circumference}`; this.progressCircle.style.strokeDashoffset = this.circumference; this.progressCircle.style.transition = 'stroke-dashoffset 0.5s ease-out, stroke 0.5s ease'; svg.appendChild(this.progressCircle); this.container.appendChild(svg); }, show(message = '开始后台扫描...') { if (!this.ring) return; this.ring.style.display = 'block'; requestAnimationFrame(() => this.ring.style.opacity = '1'); this.mainBall.style.animation = 'contribution-pulse 1.5s infinite'; this.mainBall.title = message; this.update(0, 1); }, update(current, total, courseName = '') { if (!this.ring) return; const percent = (current / total) * 100; const offset = this.circumference - (percent / 100) * this.circumference; this.progressCircle.style.strokeDashoffset = offset; this.mainBall.title = `[${current}/${total}] 正在扫描: ${courseName}`; }, complete(message) { if (!this.ring) return; this.progressCircle.style.stroke = '#22c55e'; this.mainBall.title = message; this._fadeOut(); }, error(message) { if (!this.ring) return; this.progressCircle.style.stroke = '#ef4444'; this.mainBall.title = `错误: ${message}`; this._fadeOut(3000); }, hide() { if (!this.ring) return; this._fadeOut(); }, _fadeOut(delay = 1500) { setTimeout(() => { this.ring.style.opacity = '0'; this.mainBall.style.animation = ''; setTimeout(() => { this.ring.style.display = 'none'; this.progressCircle.style.stroke = '#4F46E5'; this.mainBall.title = this.originalTitle; }, 400); }, delay); } }; const style = document.createElement('style'); style.textContent = ` @keyframes contribution-pulse { 0% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.5); } 70% { box-shadow: 0 0 0 10px rgba(79, 70, 229, 0); } 100% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0); } } `; document.head.appendChild(style); const { Document, Packer, Paragraph, HeadingLevel, AlignmentType, ImageRun, TextRun } = window.docx; function createDocxHyperlink(url, label) { try { const linkText = label || url; const d = window.docx || {}; if (d.ExternalHyperlink) { return new d.ExternalHyperlink({ link: url, children: [new TextRun({ text: linkText, style: 'Hyperlink' })], }); } if (d.Hyperlink) { return new d.Hyperlink({ link: url, children: [new TextRun({ text: linkText, style: 'Hyperlink' })] }); } } catch (e) { console.warn('创建超链接组件失败,回退为普通文本。', e); } return new TextRun({ text: label || url, color: '0000EE', underline: {} }); } let autoFetchEnabled = localStorage.getItem('autoFetchEnabled') === 'true'; let autoFillEnabled = localStorage.getItem('autoFillEnabled') === 'true'; let autoContributeEnabled = localStorage.getItem('autoContributeEnabled') !== 'false'; let isProcessing = false; let currentBatchAbortController = null; const activeAIControllers = new Set(); let debounceTimer = null; let sttCache = {}; const latexImageCache = new Map(); const mediaProcessingLocks = {}; let videoCache = {}; const videoProcessingLocks = {}; const backgroundTaskManager = { isTaskRunning: false, isTaskScheduled: false, schedule() { if (sessionStorage.getItem('xiaoya_full_scan_done') === 'true') { console.log('[后台任务调度器] 本次会话已完成全量扫描,不再调度新任务。'); return; } if (this.isTaskRunning || this.isTaskScheduled) { console.log('[后台任务调度器] 任务已在运行或计划中,忽略新的调度请求。'); return; } console.log('[后台任务调度器] 收到新的后台任务请求,将在3秒后执行...'); this.isTaskScheduled = true; setTimeout(async () => { if (this.isTaskRunning || sessionStorage.getItem('xiaoya_full_scan_done') === 'true') { console.log('[后台任务调度器] 延迟后发现任务已运行或已完成,取消本次执行。'); this.isTaskScheduled = false; return; } this.isTaskRunning = true; this.isTaskScheduled = false; try { const scanCompleted = await backgroundContributeAllCourses(); if (scanCompleted) { this.markAsCompleted(); } } catch (error) { console.error('[后台任务调度器] 后台任务执行时发生未捕获的错误:', error); } finally { this.isTaskRunning = false; console.log('[后台任务调度器] 后台任务执行完毕,状态重置为空闲。'); } }, 3000); }, markAsCompleted() { console.log('[后台任务调度器] 全量扫描已成功完成,本次会话将不再触发。'); sessionStorage.setItem('xiaoya_full_scan_done', 'true'); } }; function registerAIController(controller) { if (!controller) return; activeAIControllers.add(controller); console.log(`注册了一个新的AI AbortController,当前总数: ${activeAIControllers.size}`); controller.signal.addEventListener('abort', () => { activeAIControllers.delete(controller); console.log(`一个AI AbortController已中止并移除,剩余总数: ${activeAIControllers.size}`); }, { once: true }); } function cancelAllAITasks() { console.log(`正在取消 ${activeAIControllers.size} 个活动的AI任务...`); activeAIControllers.forEach(controller => { if (!controller.signal.aborted) { controller.abort(); } }); activeAIControllers.clear(); if (currentBatchAbortController) { currentBatchAbortController = null; } } function areAITasksRunning() { return Array.from(activeAIControllers).some(c => !c.signal.aborted); } function getToken() { const cookies = document.cookie.split('; '); for (let cookie of cookies) { const [name, value] = cookie.split('='); if (name.includes('prd-access-token')) { return value; } } return null; } async function isUrlHealthy(url) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); const response = await fetch(url, { method: 'HEAD', mode: 'cors', signal: controller.signal }); clearTimeout(timeoutId); if (response.status < 500) { console.log(`[健康检查] ✅ ${url} - 状态: ${response.status} (可用)`); return true; } else { console.warn(`[健康检查] ❌ ${url} - 状态: ${response.status} (服务器错误)`); return false; } } catch (error) { if (error.name === 'AbortError') { console.warn(`[健康检查] ❌ ${url} - 请求超时`); } else { console.warn(`[健康检查] ❌ ${url} - 连接失败: ${error.message}`); } return false; } } async function getApiBaseUrl() { const now = Date.now(); if (SCRIPT_CONFIG.cachedApiBaseUrl && (now - SCRIPT_CONFIG.lastFetchTimestamp < SCRIPT_CONFIG.cacheDuration)) { return SCRIPT_CONFIG.cachedApiBaseUrl; } if (SCRIPT_CONFIG.priorityApiBaseUrl) { HealthCheckVisualizer.addGroup('priority', '⚡️ 优先线路检测...', [SCRIPT_CONFIG.priorityApiBaseUrl], true); HealthCheckVisualizer.updateDot('priority', 0, 'testing'); if (await isUrlHealthy(SCRIPT_CONFIG.priorityApiBaseUrl)) { HealthCheckVisualizer.updateDot('priority', 0, 'success'); HealthCheckVisualizer.updateGroupLabel('priority', '✅ 优先线路连接成功!'); console.log(`[优先线路] ${SCRIPT_CONFIG.priorityApiBaseUrl} 已选定!`); SCRIPT_CONFIG.cachedApiBaseUrl = SCRIPT_CONFIG.priorityApiBaseUrl; SCRIPT_CONFIG.lastFetchTimestamp = now; setTimeout(() => HealthCheckVisualizer.destroy(), 1200); return SCRIPT_CONFIG.priorityApiBaseUrl; } else { HealthCheckVisualizer.updateDot('priority', 0, 'failure'); HealthCheckVisualizer.updateGroupLabel('priority', '❌ 优先线路不可用'); console.warn(`[优先线路] ${SCRIPT_CONFIG.priorityApiBaseUrl} 不可用,回退至动态获取...`); } } for (const url of SCRIPT_CONFIG.remoteConfigUrls) { try { const response = await fetch(url, { cache: 'no-cache' }); if (!response.ok) throw new Error(`状态: ${response.status}`); const config = await response.json(); if (config && Array.isArray(config.baseUrls) && config.baseUrls.length > 0) { HealthCheckVisualizer.addGroup('dynamic', '🌐 动态节点扫描...', config.baseUrls); for (let i = 0; i < config.baseUrls.length; i++) { const baseUrl = config.baseUrls[i]; HealthCheckVisualizer.updateDot('dynamic', i, 'testing'); if (await isUrlHealthy(baseUrl)) { HealthCheckVisualizer.updateDot('dynamic', i, 'success'); HealthCheckVisualizer.updateGroupLabel('dynamic', '✅ 动态节点连接成功!'); console.log(`[动态配置] 域名 ${baseUrl} 健康检查通过,选定此地址!`); SCRIPT_CONFIG.cachedApiBaseUrl = baseUrl; SCRIPT_CONFIG.lastFetchTimestamp = now; setTimeout(() => HealthCheckVisualizer.destroy(), 1200); return baseUrl; } else { HealthCheckVisualizer.updateDot('dynamic', i, 'failure'); } } HealthCheckVisualizer.updateGroupLabel('dynamic', '❌ 所有动态节点均不可用'); throw new Error("域名池中的所有地址都无法连接。"); } else { throw new Error("远程配置文件格式不正确或域名池为空。"); } } catch (error) { console.warn(`[动态配置] 路标 ${url} 尝试失败:`, error.message); } } console.error('[动态配置] 所有远程路标均获取失败!'); if (SCRIPT_CONFIG.cachedApiBaseUrl) { console.log(`[动态配置] 回退至上次成功的缓存地址: ${SCRIPT_CONFIG.cachedApiBaseUrl}`); HealthCheckVisualizer.addGroup('fallback', `🔄 回退至缓存: ${SCRIPT_CONFIG.cachedApiBaseUrl}`, []); SCRIPT_CONFIG.lastFetchTimestamp = now; setTimeout(() => HealthCheckVisualizer.destroy(), 2000); return SCRIPT_CONFIG.cachedApiBaseUrl; } console.log(`[动态配置] 回退至最终的默认备用地址: ${SCRIPT_CONFIG.defaultApiBaseUrl}`); HealthCheckVisualizer.addGroup('default', `‼️ 启用最终备用线路,功能可能受限`, []); showNotification('无法连接到更新服务器,脚本将使用备用线路,功能可能受限。', { type: 'warning' }); setTimeout(() => HealthCheckVisualizer.destroy(), 3000); return SCRIPT_CONFIG.defaultApiBaseUrl; } async function getCurrentUserInfo(token) { if (!token) { return null; } try { const cachedUserInfo = sessionStorage.getItem(`userInfo_${token}`); if (cachedUserInfo) { try { const parsedInfo = JSON.parse(cachedUserInfo); if (parsedInfo && parsedInfo.cacheTimestamp && (Date.now() - parsedInfo.cacheTimestamp < 5 * 60 * 1000)) { return parsedInfo.data; } } catch (e) { sessionStorage.removeItem(`userInfo_${token}`); } } const response = await fetch(`${window.location.origin}/api/jw-starcmooc/user/currentUserInfo`, { headers: { "authorization": `Bearer ${token}`, "content-type": "application/json; charset=utf-8" }, method: "GET", credentials: "include" }); if (!response.ok) { console.error(`获取用户信息失败,状态码: ${response.status}`); return null; } const data = await response.json(); if (data.code === 200 && data.result) { try { sessionStorage.setItem(`userInfo_${token}`, JSON.stringify({ data: data.result, cacheTimestamp: Date.now() })); } catch (e) { console.warn('缓存用户信息到 sessionStorage 失败:', e); } return data.result; } else { console.warn('获取用户信息API返回非成功状态:', data); return null; } } catch (error) { console.error('获取用户信息时发生网络错误:', error); return null; } } function addButtons() { const style = document.createElement('style'); style.textContent = ` :root { --menu-bg: rgba(248, 249, 252, 0.85); --menu-border: rgba(0, 0, 0, 0.08); --menu-shadow: 0 10px 30px rgba(0, 0, 0, 0.12); --primary-color: #4F46E5; --primary-color-hover: #4338CA; --text-color: #1f2937; --text-color-secondary: #4b5569; --separator-color: #e5e7eb; --button-hover-bg: rgba(79, 70, 229, 0.05); } .xiaoya-menu-container { position: fixed; top: 150px; left: 150px; z-index: 9999; user-select: none; } .xiaoya-main-ball { width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(145deg, #6366F1, #4F46E5); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); cursor: move; display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; transition: transform 0.4s cubic-bezier(0.19, 1, 0.22, 1), box-shadow 0.3s; } .xiaoya-main-ball:not(.menu-open):hover { transform: scale(1.1); box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4); } .xiaoya-main-ball.menu-open { transform: rotate(90deg) scale(0.9); } .xiaoya-menu-panel { position: absolute; top: 80px; left: -15px; width: 300px; background: var(--menu-bg); backdrop-filter: blur(12px) saturate(180%); -webkit-backdrop-filter: blur(12px) saturate(180%); border-radius: 16px; box-shadow: var(--menu-shadow); border: 1px solid var(--menu-border); transform-origin: top left; transition: transform 0.4s cubic-bezier(0.19, 1, 0.22, 1), opacity 0.3s; opacity: 0; transform: scale(0.9) translateY(-10px); pointer-events: none; display: flex; flex-direction: column; max-height: 70vh; } .xiaoya-menu-panel.visible { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; } .xiaoya-menu-header { padding: 12px 16px; border-bottom: 1px solid var(--separator-color); cursor: move; display: flex; justify-content: space-between; align-items: center; } .xiaoya-menu-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: var(--text-color); } .xiaoya-menu-body { padding: 12px; overflow-y: auto; flex-grow: 1; } .xiaoya-menu-body::-webkit-scrollbar { width: 5px; } .xiaoya-menu-body::-webkit-scrollbar-track { background: transparent; } .xiaoya-menu-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } .xiaoya-menu-button, .xiaoya-menu-toggle { display: flex; align-items: center; width: 100%; padding: 10px 12px; border: none; background: none; text-align: left; border-radius: 8px; cursor: pointer; transition: background-color 0.2s, color 0.2s; font-size: 14px; color: var(--text-color-secondary); } .xiaoya-menu-button:hover, .xiaoya-menu-toggle:hover { background-color: var(--button-hover-bg); color: var(--primary-color); } .xiaoya-menu-icon { font-size: 18px; width: 28px; text-align: center; margin-right: 12px; } .xiaoya-menu-separator { border: none; border-top: 1px solid var(--separator-color); margin: 8px 0; } .xiaoya-menu-toggle-switch { margin-left: auto; width: 42px; height: 24px; background-color: #e5e7eb; border-radius: 12px; position: relative; transition: background-color 0.3s; } .xiaoya-menu-toggle-switch::before { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background-color: white; border-radius: 50%; transition: transform 0.3s cubic-bezier(0.19, 1, 0.22, 1); box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .xiaoya-menu-toggle input:checked + .xiaoya-menu-toggle-switch { background-color: var(--primary-color); } .xiaoya-menu-toggle input:checked + .xiaoya-menu-toggle-switch::before { transform: translateX(18px); } .xiaoya-menu-toggle input { display: none; } .xiaoya-menu-button.special-action { background: linear-gradient(135deg, rgba(79, 70, 229, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); color: var(--primary-color); font-weight: 500; } .xiaoya-menu-button.special-action:hover { background: linear-gradient(135deg, rgba(79, 70, 229, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%); } `; document.head.appendChild(style); const container = document.createElement('div'); container.className = 'xiaoya-menu-container'; const mainBall = document.createElement('div'); mainBall.className = 'xiaoya-main-ball'; mainBall.innerHTML = '✨'; const panel = document.createElement('div'); panel.className = 'xiaoya-menu-panel'; const header = document.createElement('div'); header.className = 'xiaoya-menu-header'; header.innerHTML = '

小雅答答答

'; const body = document.createElement('div'); body.className = 'xiaoya-menu-body'; panel.appendChild(header); panel.appendChild(body); container.appendChild(mainBall); container.appendChild(panel); document.body.appendChild(container); ContributionProgressUI.init(container); const buttonsConfig = [ { id: 'get-answers', icon: '🕷️', text: '获取答案 / 激活', onClick: () => getAndStoreAnswers(true), type: 'button', special: true }, { id: 'get-submitted', icon: '📜', text: '获取已提交作业', onClick: () => getSubmittedAnswers(), type: 'button' }, { id: 'fill-answers', icon: '✍️', text: '填写答案', onClick: () => fillAnswers(), type: 'button', special: true }, { id: 'view-edit', icon: '🖋️', text: '查看 / 编辑答案', onClick: () => showAnswerEditor(), type: 'button', special: true }, { id: 'export-hw', icon: '📄', text: '导出作业为 Word', onClick: () => exportHomework(), type: 'button' }, { id: 'export-hw-md', icon: 'Ⓜ️', text: '导出作业为 Markdown', onClick: () => exportHomeworkMarkdown(), type: 'button' }, { type: 'separator' }, { id: 'auto-fetch', icon: { enabled: '🔄', disabled: '⭕' }, text: '自动获取答案', state: () => autoFetchEnabled, onClick: (el, iconEl) => { autoFetchEnabled = !autoFetchEnabled; localStorage.setItem('autoFetchEnabled', autoFetchEnabled); el.querySelector('input').checked = autoFetchEnabled; iconEl.textContent = autoFetchEnabled ? '🔄' : '⭕'; }, type: 'toggle' }, { id: 'auto-fill', icon: { enabled: '🔄', disabled: '⭕' }, text: '自动填写答案', state: () => autoFillEnabled, onClick: (el, iconEl) => { autoFillEnabled = !autoFillEnabled; localStorage.setItem('autoFillEnabled', autoFillEnabled); el.querySelector('input').checked = autoFillEnabled; iconEl.textContent = autoFillEnabled ? '🔄' : '⭕'; }, type: 'toggle' }, { type: 'separator' }, { id: 'ai-settings', icon: '⚙️', text: 'AI 设置', onClick: () => showAISettingsPanel(), type: 'button', special: true }, { id: 'check-usage', icon: '📊', text: '检查用量', onClick: () => checkUsage(), type: 'button' }, { id: 'show-guide', icon: '🧭', text: '使用指南', onClick: () => showTutorial(), type: 'button', special: true }, { type: 'separator' }, { id: 'contribute-current', icon: '💝', text: '贡献当前作业', onClick: async () => { if (!(await checkAccountConsistency())) { showNotification('操作中止:当前登录账号与脚本激活账号不一致。', { type: 'error', duration: 5000 }); return; } if (!(await isTaskPage())) { showNotification('当前不是有效的作业/测验页面,无法进行贡献。', { type: 'warning' }); return; } const groupId = getGroupIDFromUrl(window.location.href); const nodeId = getNodeIDFromUrl(window.location.href); if (!groupId || !nodeId) { showNotification('无法获取页面参数,操作中止。', { type: 'error' }); return; } showNotification('正在贡献答案到题库...', { type: 'info', duration: 5000 }); try { const result = await contributeSingleAssignment(groupId, nodeId); if (result.success) { showNotification(`✅ 贡献成功: ${result.message}`, { type: 'success', duration: 8000 }); } else { showNotification(`❌ 贡献失败: ${result.error}`, { type: 'error', duration: 8000 }); } } catch (error) { showNotification(`💥 贡献答案时发生严重错误: ${error.message}`, { type: 'error' }); } }, type: 'button' }, { id: 'auto-contribute', icon: { enabled: '💖', disabled: '🤍' }, text: '自动贡献答案', state: () => autoContributeEnabled, onClick: async (el, iconEl) => { if (autoContributeEnabled) { const confirmedToKeep = await showConfirmNotification('感谢你一直以来的贡献!💖', { animation: 'scale', confirmText: '继续贡献', cancelText: '仍要关闭', title: '请留步,有几句话想对你说', description: `

你开启的“自动贡献”功能是我们答案库成长的基石。每一次贡献,都在帮助更多和你一样的同学。

郑重承诺:

` }); if (confirmedToKeep) { showNotification('非常感谢!自动贡献功能将保持开启。', { type: 'success', animation: 'scale' }); el.querySelector('input').checked = true; iconEl.textContent = '💖'; return; } } autoContributeEnabled = !autoContributeEnabled; localStorage.setItem('autoContributeEnabled', autoContributeEnabled); el.querySelector('input').checked = autoContributeEnabled; iconEl.textContent = autoContributeEnabled ? '💖' : '🤍'; if (autoContributeEnabled) { showNotification('后台自动贡献功能已开启。脚本将在后台为你扫描并贡献所有课程的答案。', { type: 'info' }); sessionStorage.removeItem('xiaoya_full_scan_done'); backgroundTaskManager.schedule(); } else { showNotification('自动贡献功能已关闭。感谢你曾经的付出!', { type: 'info' }); } }, type: 'toggle' }, ]; buttonsConfig.forEach(config => { if (config.type === 'separator') { body.appendChild(document.createElement('hr')).className = 'xiaoya-menu-separator'; return; } if (config.type === 'button') { const button = document.createElement('button'); button.className = 'xiaoya-menu-button'; if (config.special) button.classList.add('special-action'); button.innerHTML = ` ${config.icon} ${config.text} `; button.onclick = config.onClick; body.appendChild(button); } else if (config.type === 'toggle') { const label = document.createElement('label'); label.className = 'xiaoya-menu-toggle'; const isEnabled = config.state(); label.innerHTML = ` ${isEnabled ? config.icon.enabled : config.icon.disabled} ${config.text}
`; const iconSpan = label.querySelector('.xiaoya-menu-icon'); label.onclick = (e) => { e.preventDefault(); config.onClick(label, iconSpan); }; body.appendChild(label); } }); let isPanelVisible = false; function togglePanel() { isPanelVisible = !isPanelVisible; panel.classList.toggle('visible', isPanelVisible); mainBall.classList.toggle('menu-open', isPanelVisible); } mainBall.addEventListener('click', (e) => { if (!hasDragged) { togglePanel(); } }); let isDragging = false, hasDragged = false; let initialX, initialY, xOffset = 0, yOffset = 0; const dragThreshold = 5; function dragStart(e) { hasDragged = false; const target = e.target; if (target === mainBall || target === header || header.contains(target)) { isDragging = true; const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; const clientY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY; xOffset = clientX - container.offsetLeft; yOffset = clientY - container.offsetTop; initialX = clientX; initialY = clientY; } } function drag(e) { if (isDragging) { e.preventDefault(); const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY; if (!hasDragged) { const dx = clientX - initialX; const dy = clientY - initialY; if (Math.sqrt(dx * dx + dy * dy) > dragThreshold) { hasDragged = true; } } let newX = clientX - xOffset; let newY = clientY - yOffset; const containerRect = container.getBoundingClientRect(); newX = Math.max(0, Math.min(newX, window.innerWidth - containerRect.width)); newY = Math.max(0, Math.min(newY, window.innerHeight - containerRect.height)); container.style.left = newX + 'px'; container.style.top = newY + 'px'; } } function dragEnd() { isDragging = false; setTimeout(() => { hasDragged = false; }, 0); } header.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); header.addEventListener('touchstart', dragStart, { passive: true }); document.addEventListener('touchmove', drag, { passive: false }); document.addEventListener('touchend', dragEnd); mainBall.addEventListener('mousedown', dragStart); mainBall.addEventListener('touchstart', dragStart, { passive: true }); } function createProgressBar() { const style = document.createElement('style'); style.textContent = ` .answer-progress { position: fixed; top: 0; left: 0; width: 100%; height: 6px; background: rgba(0, 0, 0, 0.05); z-index: 10000; opacity: 0; transition: opacity 0.4s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); pointer-events: none; } .answer-progress-bar { height: 100%; background: linear-gradient(90deg, #60a5fa, #818cf8); width: 0%; transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); border-radius: 0 3px 3px 0; box-shadow: 0 0 8px rgba(96, 165, 250, 0.5); } .answer-progress-text { position: fixed; top: 12px; right: 20px; transform: translateY(-10px); background: #4f46e5; color: white; padding: 6px 12px; border-radius: 6px; font-size: 13px; opacity: 0; transition: all 0.4s ease; box-shadow: 0 2px 6px rgba(79, 70, 229, 0.3); font-weight: bold; pointer-events: none; } `; document.head.appendChild(style); const progressContainer = document.createElement('div'); progressContainer.className = 'answer-progress'; const progressBar = document.createElement('div'); progressBar.className = 'answer-progress-bar'; const progressText = document.createElement('div'); progressText.className = 'answer-progress-text'; progressContainer.appendChild(progressBar); document.body.appendChild(progressContainer); document.body.appendChild(progressText); return { show: () => { progressContainer.style.opacity = '1'; progressText.style.opacity = '1'; progressText.style.transform = 'translateY(0)'; }, hide: () => { progressContainer.style.opacity = '0'; progressText.style.opacity = '0'; progressText.style.transform = 'translateY(-10px)'; setTimeout(() => { progressContainer.remove(); progressText.remove(); }, 300); }, update: (current, total, action = '正在填写', unit = '题') => { const percent = total > 0 ? (current / total) * 100 : 0; progressBar.style.width = percent + '%'; const displayCurrent = Math.round(current); const unitString = unit ? ` ${unit}` : ''; progressText.textContent = `${action}: ${displayCurrent}/${total}${unitString}`; }, }; } addButtons(); function addGlobalStyles() { const style = document.createElement('style'); style.textContent = ` .code-editor-wrapper { position: relative; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; } .code-editor-wrapper textarea { font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important; letter-spacing: 0.3px; } .code-editor-wrapper textarea::-webkit-scrollbar { width: 12px; height: 12px; } .code-editor-wrapper textarea::-webkit-scrollbar-track { background: #1e1e1e; } .code-editor-wrapper textarea::-webkit-scrollbar-thumb { background: #424242; border-radius: 6px; } .code-editor-wrapper textarea::-webkit-scrollbar-thumb:hover { background: #4e4e4e; } .code-line-numbers::-webkit-scrollbar { display: none; } .code-editor-wrapper::before { content: attr(data-language); position: absolute; top: 8px; right: 12px; background: rgba(99, 102, 241, 0.9); color: white; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; z-index: 10; pointer-events: none; text-transform: uppercase; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .image-upload-btn, .ai-assist-btn, .quark-search-btn{ padding: 8px 16px; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold; display: flex; align-items: center; gap: 6px; transition: all 0.2s ease; height: 36px; } .image-upload-btn { background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); box-shadow: 0 2px 4px rgba(22, 163, 74, 0.3); } .image-upload-btn:hover { transform: translateY(-1px); background: linear-gradient(135deg, #16a34a 0%, #15803d 100%); box-shadow: 0 4px 8px rgba(22, 163, 74, 0.4); } .image-upload-btn:active { transform: translateY(1px); } .image-upload-btn.loading, .ai-assist-btn.loading, .quark-search-btn.loading { background: #9ca3af; cursor: not-allowed; opacity: 0.8; } .image-upload-btn .icon, .ai-assist-btn .icon, .quark-search-btn .icon { font-size: 16px; } .image-upload-btn.loading .icon, .ai-assist-btn.loading .icon, .quark-search-btn.loading .icon { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .ai-assist-btn { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); box-shadow: 0 2px 4px rgba(79, 70, 229, 0.1); } .ai-assist-btn:hover { transform: translateY(-1px); background: linear-gradient(135deg, #4338ca 0%, #4f46e5 100%); box-shadow: 0 4px 8px rgba(79, 70, 229, 0.2); } .ai-assist-btn:active { transform: translateY(1px); } .quark-search-btn { background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%); box-shadow: 0 2px 4px rgba(14, 165, 233, 0.1); } .quark-search-btn:hover { transform: translateY(-1px); background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%); box-shadow: 0 4px 8px rgba(14, 165, 233, 0.2); } .quark-search-btn:active { transform: translateY(1px); } .char-count { font-size: 12px; color: #6b7280; margin-left: auto; padding: 4px 8px; background-color: #f9fafb; border-radius: 6px; border: 1px solid #e5e7eb; transition: all 0.2s ease; } .char-count.active { color: #4f46e5; border-color: #c7d2fe; background-color: #eef2ff; } .ai-thinking-process details { border: none !important; background: none !important; } .ai-thinking-process summary { padding: 8px 0px !important; } .ai-thinking-process .timeline-container { padding: 10px 10px; max-height: 300px; overflow-y: auto; scrollbar-width: thin; } .ai-thinking-process .timeline-container::-webkit-scrollbar { width: 5px; } .ai-thinking-process .timeline-container::-webkit-scrollbar-track { background: transparent; } .ai-thinking-process .timeline-container::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } .ai-thinking-process .timeline-step { position: relative; padding-left: 30px; padding-bottom: 20px; } .ai-thinking-process .timeline-step:last-child { padding-bottom: 5px; } .ai-thinking-process .timeline-step.completed::before { content: ''; position: absolute; left: 7px; top: 8px; width: 2px; height: 100%; background-color: #6366f1; z-index: 1; transition: background-color 0.3s ease; } .ai-thinking-process .timeline-marker { position: absolute; left: 0; top: 0; width: 16px; height: 16px; border-radius: 50%; background-color: #e5e7eb; border: 3px solid #f9fafb; transition: all 0.3s ease; z-index: 1; } .ai-thinking-process .timeline-content h4 { margin: 0 0 5px 0; font-size: 14px; font-weight: 600; color: #4b5569; transition: color 0.3s ease; } .ai-thinking-process .timeline-content p { margin: 0; font-size: 13px; color: #6b7280; line-height: 1.6; transition: all 0.3s ease; max-height: 0; opacity: 0.5; } .ai-thinking-process .timeline-content ul, .ai-thinking-process .timeline-content ol { padding-left: 20px; margin: 8px 0; } .ai-thinking-process .timeline-content li { margin-bottom: 4px; } .ai-thinking-process .timeline-content h2, .ai-thinking-process .timeline-content h3, .ai-thinking-process .timeline-content h4 { margin: 12px 0 6px 0; color: #1f2937; font-weight: 600; } .ai-thinking-process .timeline-content strong { font-weight: 600; color: #374151; } .ai-thinking-process .timeline-step.completed .timeline-marker { background-color: #6366f1; border-color: #eef2ff; } .ai-thinking-process .timeline-step.completed .timeline-content p { max-height: 300px; opacity: 0.7; } .ai-thinking-process .timeline-step.active .timeline-marker { background-color: #4f46e5; transform: scale(1.2); border-color: #e0e7ff; animation: ai-thinking-pulse 1.5s infinite; } .ai-thinking-process .timeline-step.active .timeline-content h4 { color: #1f2937; } .ai-thinking-process .timeline-step.active .timeline-content p { max-height: 500px; opacity: 1; } @keyframes ai-thinking-pulse { 0% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.4); } 70% { box-shadow: 0 0 0 8px rgba(79, 70, 229, 0); } 100% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0); } } @keyframes contentFadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } `; document.head.appendChild(style); } addGlobalStyles(); class NotificationAnimator { static animations = { fadeSlide: { enter: { initial: { opacity: '0', transform: 'translateY(-20px)' }, final: { opacity: '1', transform: 'translateY(0)' } }, exit: { initial: { opacity: '1', transform: 'translateY(0)' }, final: { opacity: '0', transform: 'translateY(-20px)' } } }, scale: { enter: { initial: { opacity: '0', transform: 'scale(0.8)' }, final: { opacity: '1', transform: 'scale(1)' } }, exit: { initial: { opacity: '1', transform: 'scale(1)' }, final: { opacity: '0', transform: 'scale(0.8)' } } }, slideRight: { enter: { initial: { opacity: '0', transform: 'translateX(-100%)' }, final: { opacity: '1', transform: 'translateX(0)' } }, exit: { initial: { opacity: '1', transform: 'translateX(0)' }, final: { opacity: '0', transform: 'translateX(100%)' } } } }; static applyAnimation(element, animationType, isEnter) { const animation = this.animations[animationType]; if (!animation) return; const { initial, final } = isEnter ? animation.enter : animation.exit; Object.assign(element.style, { transition: 'all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55)', ...initial }); requestAnimationFrame(() => { Object.assign(element.style, final); }); } } function getNotificationContainer() { let container = document.getElementById('notification-container'); if (!container) { container = document.createElement('div'); container.id = 'notification-container'; container.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 100000; max-height: calc(100vh - 40px); overflow-y: auto; overflow-x: hidden; pointer-events: none; display: flex; flex-direction: column; align-items: center; opacity: 0; transition: opacity 0.3s ease; `; document.body.appendChild(container); container.offsetHeight; container.style.opacity = '1'; } return container; } function showNotification(message, options = {}) { const { type = 'info', duration = 3000, keywords = [], animation = 'fadeSlide', id = null } = options; const container = getNotificationContainer(); const existingNotifications = container.querySelectorAll('.message-container'); for (let i = 0; i < existingNotifications.length; i++) { if (existingNotifications[i].textContent === message) { return; } } if (id) { let existingNotification = container.querySelector(`[data-notification-id="${id}"]`); if (existingNotification) { const messageContainer = existingNotification.querySelector('.message-container'); const icon = existingNotification.querySelector('.notification-icon'); const typeStyles = { success: { icon: '🎉' }, error: { icon: '❌' }, warning: { icon: '⚠️' }, info: { icon: 'ℹ️' } }; const currentType = typeStyles[type] || typeStyles.info; if (icon) icon.textContent = currentType.icon; if (messageContainer) messageContainer.innerHTML = message; if (duration > 0) { if (existingNotification.hideTimeout) clearTimeout(existingNotification.hideTimeout); existingNotification.hideTimeout = setTimeout(() => { hideNotification(existingNotification); }, duration); } return; } } const highlightColors = { success: '#ffba08', error: '#14b8a6', warning: '#8b5cf6', info: '#f472b6' }; const highlightColor = highlightColors[type] || highlightColors.info; const highlightStyle = ` color: ${highlightColor}; font-weight: bold; border-bottom: 2px solid ${highlightColor}50; transition: all 0.3s ease; border-radius: 3px; `; let highlightedMessage = message; if (keywords && keywords.length > 0) { const uniqueKeywords = [...new Set(keywords)].map(k => String(k).trim()).filter(Boolean); if (uniqueKeywords.length > 0) { uniqueKeywords.sort((a, b) => b.length - a.length); const escapedKeywords = uniqueKeywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const regex = new RegExp(`\\b(${escapedKeywords.join('|')})\\b`, 'g'); highlightedMessage = message.replace(regex, (match) => `${match}` ); } } const notification = document.createElement('div'); if (id) notification.dataset.notificationId = id; notification.style.cssText = ` position: relative; margin-bottom: 10px; padding: 15px 20px; border-radius: 12px; color: #333; font-size: 16px; font-weight: bold; box-shadow: 0 8px 16px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.06); pointer-events: auto; opacity: 0; transform: translateY(-20px); transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); display: flex; align-items: center; backdrop-filter: blur(8px); `; const typeStyles = { success: { background: 'linear-gradient(145deg, rgba(104, 214, 156, 0.95), rgba(89, 186, 134, 0.95))', icon: '🎉' }, error: { background: 'linear-gradient(145deg, rgba(248, 113, 113, 0.95), rgba(220, 38, 38, 0.95))', icon: '❌' }, warning: { background: 'linear-gradient(145deg, rgba(251, 191, 36, 0.95), rgba(245, 158, 11, 0.95))', icon: '⚠️' }, info: { background: 'linear-gradient(145deg, rgba(96, 165, 250, 0.95), rgba(59, 130, 246, 0.95))', icon: 'ℹ️' } }; const currentType = typeStyles[type] || typeStyles.info; notification.style.background = currentType.background; notification.style.color = type === 'info' || type === 'success' ? '#fff' : '#000'; const progressBar = document.createElement('div'); progressBar.style.cssText = ` position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; background: rgba(255, 255, 255, 0.3); border-radius: 0 0 12px 12px; transition: width ${duration}ms cubic-bezier(0.4, 0, 0.2, 1); `; const icon = document.createElement('span'); icon.className = 'notification-icon'; icon.style.cssText = 'margin-right: 12px; font-size: 20px; filter: saturate(1.2);'; icon.textContent = currentType.icon; const messageContainer = document.createElement('div'); messageContainer.className = 'message-container'; messageContainer.innerHTML = highlightedMessage; messageContainer.textContent = message; messageContainer.style.cssText = 'flex: 1; font-weight: bold;'; const closeButton = document.createElement('button'); closeButton.innerHTML = ``; closeButton.style.cssText = ` margin-left: 12px; background: #f3f4f6; border: none; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; color: #6b7280; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; flex-shrink: 0; `; closeButton.onmouseover = () => { }; closeButton.onmouseout = () => { }; notification.appendChild(icon); notification.appendChild(messageContainer); notification.appendChild(closeButton); notification.appendChild(progressBar); container.prepend(notification); requestAnimationFrame(() => { NotificationAnimator.applyAnimation(notification, animation, true); if (duration > 0) { requestAnimationFrame(() => { progressBar.style.width = '0'; }); } }); function hideNotification(notificationElement) { if (!container.contains(notificationElement)) return; NotificationAnimator.applyAnimation(notificationElement, animation, false); setTimeout(() => { if (container.contains(notificationElement)) { container.removeChild(notificationElement); } if (container.children.length === 0 && document.body.contains(container)) { document.body.removeChild(container); } }, 300); } const hideThisNotification = () => hideNotification(notification); closeButton.addEventListener('click', (e) => { e.stopPropagation(); clearTimeout(notification.hideTimeout); hideThisNotification(); }); if (duration > 0) { notification.addEventListener('click', hideThisNotification); notification.hideTimeout = setTimeout(() => { hideThisNotification(); }, duration); } } function showConfirmNotification(message, options = {}) { const { animation = 'scale', confirmText = '确认', cancelText = '取消', title = null, description = null } = options; return new Promise((resolve) => { const container = getNotificationContainer(); const notification = document.createElement('div'); notification.style.cssText = ` position: relative; margin-bottom: 10px; padding: 20px 25px; border-radius: 16px; color: #333; font-size: 16px; font-weight: bold; box-shadow: 0 10px 25px rgba(0,0,0,0.1), 0 5px 10px rgba(0,0,0,0.05); pointer-events: auto; opacity: 0; transform: translateY(-20px); transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); display: flex; flex-direction: column; gap: 15px; background: linear-gradient(145deg, #ffffff, #f8f9fa); backdrop-filter: blur(8px); border: 1px solid rgba(0,0,0,0.05); max-width: 450px; `; if (title) { const titleDiv = document.createElement('h3'); titleDiv.textContent = title; titleDiv.style.cssText = ` margin: 0; font-size: 18px; font-weight: 700; color: #1f2937; text-align: center; `; notification.appendChild(titleDiv); } const messageDiv = document.createElement('div'); messageDiv.innerHTML = message; messageDiv.style.fontWeight = '600'; messageDiv.style.textAlign = 'center'; notification.appendChild(messageDiv); if (description) { const descriptionDiv = document.createElement('div'); descriptionDiv.innerHTML = description; descriptionDiv.style.cssText = ` margin-top: 5px; font-size: 15px; font-weight: normal; color: #4b5569; line-height: 1.5; text-align: center; `; notification.appendChild(descriptionDiv); } const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 12px; justify-content: center; margin-top: 10px; `; const confirmBtn = document.createElement('button'); confirmBtn.textContent = confirmText; confirmBtn.style.cssText = ` padding: 8px 18px; border: none; border-radius: 8px; background: #4f46e5; color: white; cursor: pointer; font-weight: bold; transition: all 0.2s ease; `; const cancelBtn = document.createElement('button'); cancelBtn.textContent = cancelText; cancelBtn.style.cssText = ` padding: 8px 18px; border: 1px solid #d1d5db; border-radius: 8px; background: transparent; color: #4b5569; cursor: pointer; font-weight: bold; transition: all 0.2s ease; `; [confirmBtn, cancelBtn].forEach(btn => { btn.onmouseover = () => { btn.style.transform = 'translateY(-1px)'; btn.style.filter = 'brightness(1.1)'; }; btn.onmouseout = () => { btn.style.transform = 'translateY(0)'; btn.style.filter = 'brightness(1)'; }; }); buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(confirmBtn); notification.appendChild(buttonContainer); container.appendChild(notification); requestAnimationFrame(() => { notification.style.opacity = '1'; notification.style.transform = 'translateY(0)'; }); requestAnimationFrame(() => { requestAnimationFrame(() => { NotificationAnimator.applyAnimation(notification, animation, true); }); }); const hideNotification = (result) => { NotificationAnimator.applyAnimation(notification, animation, false); setTimeout(() => { if (container.contains(notification)) { container.removeChild(notification); } if (container.children.length === 0 && document.body.contains(container)) { document.body.removeChild(container); } resolve(result); }, 300); }; confirmBtn.onclick = () => hideNotification(true); cancelBtn.onclick = () => hideNotification(false); }); } function promptActivationCode() { const modalOverlay = document.createElement('div'); modalOverlay.style.position = 'fixed'; modalOverlay.style.top = '0'; modalOverlay.style.left = '0'; modalOverlay.style.width = '100%'; modalOverlay.style.height = '100%'; modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)'; modalOverlay.style.zIndex = '9999'; modalOverlay.style.display = 'flex'; modalOverlay.style.alignItems = 'center'; modalOverlay.style.justifyContent = 'center'; modalOverlay.style.opacity = '0'; modalOverlay.style.transition = 'opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1)'; modalOverlay.style.backdropFilter = 'blur(8px)'; const modalContainer = document.createElement('div'); modalContainer.style.backgroundColor = '#ffffff'; modalContainer.style.padding = '40px'; modalContainer.style.borderRadius = '20px'; modalContainer.style.boxShadow = '0 20px 50px rgba(0,0,0,0.15), 0 0 20px rgba(0,0,0,0.1)'; modalContainer.style.width = '420px'; modalContainer.style.maxWidth = '90%'; modalContainer.style.textAlign = 'center'; modalContainer.style.position = 'relative'; modalContainer.style.transform = 'scale(0.8) translateY(20px)'; modalContainer.style.transition = 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)'; modalContainer.style.border = '1px solid rgba(255, 255, 255, 0.1)'; const modalHeader = document.createElement('div'); modalHeader.style.marginBottom = '30px'; modalHeader.style.position = 'relative'; const icon = document.createElement('div'); icon.innerHTML = ` `; icon.style.marginBottom = '15px'; icon.style.color = '#4CAF50'; const closeButton = document.createElement('button'); closeButton.innerHTML = ` `; closeButton.style.cssText = ` position: absolute; top: 15px; right: 15px; background: #f3f4f6; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; color: #6b7280; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.08); `; closeButton.onmouseover = () => { closeButton.style.backgroundColor = '#e5e7eb'; closeButton.style.transform = 'rotate(90deg)'; closeButton.style.color = '#000'; closeButton.style.boxShadow = '0 4px 8px rgba(0,0,0,0.12)'; }; closeButton.onmouseout = () => { closeButton.style.backgroundColor = '#f3f4f6'; closeButton.style.transform = 'rotate(0deg)'; closeButton.style.color = '#6b7280'; closeButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.08)'; }; const title = document.createElement('h2'); title.textContent = '输入激活码'; title.style.fontSize = '24px'; title.style.fontWeight = '600'; title.style.color = '#333'; title.style.margin = '0 0 8px 0'; const subtitle = document.createElement('p'); subtitle.textContent = '请输入激活码以继续使用完整功能'; subtitle.style.color = '#666'; subtitle.style.fontSize = '14px'; subtitle.style.margin = '0'; const infoMessage = document.createElement('p'); infoMessage.innerHTML = '关于激活码获取,请移步我的主页或者直接访问爱发电'; infoMessage.style.color = '#666'; infoMessage.style.fontSize = '14px'; infoMessage.style.margin = '10px 0 0 0'; const reuseInfoBox = document.createElement('div'); reuseInfoBox.style.cssText = ` margin-top: 20px; padding: 15px; background-color: #f0f9ff; border: 1px solid #bae6fd; border-radius: 12px; font-size: 13.5px; line-height: 1.7; color: #0c4a6e; text-align: left; `; reuseInfoBox.innerHTML = ` 💡 重要提示 `; const inputContainer = document.createElement('div'); inputContainer.style.position = 'relative'; inputContainer.style.marginTop = '25px'; const input = document.createElement('input'); input.type = 'text'; input.placeholder = '请输入激活码'; input.style.width = '100%'; input.style.padding = '15px 20px'; input.style.border = '2px solid #e0e0e0'; input.style.borderRadius = '12px'; input.style.fontSize = '16px'; input.style.backgroundColor = '#f8f9fa'; input.style.transition = 'all 0.3s ease'; input.style.boxSizing = 'border-box'; input.style.outline = 'none'; input.addEventListener('focus', () => { input.style.border = '2px solid #4CAF50'; input.style.backgroundColor = '#ffffff'; input.style.boxShadow = '0 0 0 4px rgba(76, 175, 80, 0.1)'; }); input.addEventListener('blur', () => { input.style.border = '2px solid #e0e0e0'; input.style.backgroundColor = '#f8f9fa'; input.style.boxShadow = 'none'; }); const confirmButton = document.createElement('button'); confirmButton.textContent = '激活'; confirmButton.style.width = '100%'; confirmButton.style.padding = '15px'; confirmButton.style.marginTop = '20px'; confirmButton.style.border = 'none'; confirmButton.style.borderRadius = '12px'; confirmButton.style.backgroundColor = '#4CAF50'; confirmButton.style.color = '#fff'; confirmButton.style.fontSize = '16px'; confirmButton.style.fontWeight = '600'; confirmButton.style.cursor = 'pointer'; confirmButton.style.transition = 'all 0.3s ease'; confirmButton.style.transform = 'translateY(0)'; confirmButton.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.2)'; let isLoading = false; const setLoadingState = (loading) => { isLoading = loading; if (loading) { confirmButton.innerHTML = '验证中...'; confirmButton.style.backgroundColor = '#45a049'; confirmButton.disabled = true; } else { confirmButton.textContent = '激活'; confirmButton.style.backgroundColor = '#4CAF50'; confirmButton.disabled = false; } }; const style = document.createElement('style'); style.textContent = ` .loading { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(255,255,255,.3); border-radius: 50%; border-top-color: #fff; animation: spin 1s ease-in-out infinite; margin-right: 8px; vertical-align: middle; } `; document.head.appendChild(style); modalHeader.appendChild(icon); modalHeader.appendChild(title); modalHeader.appendChild(subtitle); modalHeader.appendChild(infoMessage); modalHeader.appendChild(reuseInfoBox); modalContainer.appendChild(modalHeader); modalContainer.appendChild(closeButton); inputContainer.appendChild(input); modalContainer.appendChild(inputContainer); modalContainer.appendChild(confirmButton); modalOverlay.appendChild(modalContainer); document.body.appendChild(modalOverlay); requestAnimationFrame(() => { modalOverlay.style.opacity = '1'; modalContainer.style.transform = 'scale(1) translateY(0)'; }); function closeModal() { modalOverlay.style.opacity = '0'; modalContainer.style.transform = 'scale(0.8) translateY(20px)'; setTimeout(() => { document.body.removeChild(modalOverlay); document.head.removeChild(style); }, 400); } closeButton.addEventListener('click', () => { closeModal(); showNotification('请输入激活码。', { type: 'warning', keywords: ['激活码'], animation: 'scale' }); }); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { closeModal(); showNotification('请输入激活码。', { type: 'warning', keywords: ['激活码'], animation: 'scale' }); } }); confirmButton.addEventListener('click', () => { const userCode = input.value.trim(); if (isLoading) return; if (userCode) { setLoadingState(true); const token = getToken(); getCurrentUserInfo(token).then(userInfo => { if (!userInfo || !userInfo.id) { showNotification('无法获取小雅用户信息,请先登录小雅。', { type: 'error' }); setLoadingState(false); return; } getApiBaseUrl().then(baseUrl => { fetch(`${baseUrl}/api/activate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ activation_code: userCode, platform_user_id: userInfo.id.toString(), xiaoyaToken: token, origin: window.location.origin }) }) .then(res => res.json()) .then(data => { setLoadingState(false); if (data.success) { localStorage.setItem('xiaoya_access_token', data.access_token); localStorage.setItem('xiaoya_refresh_token', data.refresh_token); localStorage.setItem('xiaoya_bound_user_id', userInfo.id.toString()); showNotification('激活成功!', { type: 'success', animation: 'scale' }); closeModal(); getAndStoreAnswers(); } else { showNotification(`激活失败: ${data.error}`, { type: 'error' }); } }) .catch(err => { setLoadingState(false); showNotification(`网络错误: ${err.message}`, { type: 'error' }); }); }); }); } else { input.style.border = '2px solid #ff4444'; input.style.backgroundColor = '#fff8f8'; showNotification('请输入激活码。', { type: 'warning', keywords: ['激活码'], animation: 'fadeSlide' }); input.focus(); } }); input.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !isLoading) { confirmButton.click(); } }); } async function authedFetch(action, payload) { let accessToken = localStorage.getItem('xiaoya_access_token'); if (!accessToken) { throw new Error('需要激活'); } const xiaoyaToken = getToken(); if (!xiaoyaToken) throw new Error('无法获取小雅 Token'); const currentUserInfo = await getCurrentUserInfo(xiaoyaToken); if (!currentUserInfo || !currentUserInfo.id) { showNotification('无法获取当前小雅用户信息,请确保已登录。', { type: 'error' }); throw new Error('无法获取当前小雅用户信息'); } const finalPayload = { ...payload, xiaoyaToken, origin: window.location.origin, current_platform_user_id: currentUserInfo.id.toString(), script_version: GM_info.script.version }; const baseUrl = await getApiBaseUrl(); async function doFetch(token) { return fetch(`${baseUrl}/api/action/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(finalPayload) }); } let response = await doFetch(accessToken); if (response.status === 401 || response.status === 403) { const errorData = await response.json(); const errorMessage = errorData.error || ''; if (errorData.code === 'FRAUD_DETECTED') { console.error(`[欺诈检测] 后端返回欺诈警告: ${errorMessage}`); throw new Error(`欺诈行为警告: ${errorMessage}`); } if (errorMessage.includes('重新激活') || errorMessage.includes('用户不存在') || errorMessage.includes('已到期')) { console.warn(`后端要求重新激活: ${errorMessage}`); throw new Error(`凭证失效,请重新激活: ${errorMessage}`); } if (errorData.code === 'TOKEN_EXPIRED') { console.log('Access Token 过期,尝试刷新...'); const refreshToken = localStorage.getItem('xiaoya_refresh_token'); if (!refreshToken) { throw new Error('刷新令牌不存在,请重新激活'); } const refreshResponse = await fetch(`${baseUrl}/api/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken }) }); if (refreshResponse.ok) { const refreshData = await refreshResponse.json(); accessToken = refreshData.access_token; localStorage.setItem('xiaoya_access_token', accessToken); console.log('Token 刷新成功,重试请求...'); response = await doFetch(accessToken); } else { const refreshErrorData = await refreshResponse.json(); const message = refreshErrorData.error || '刷新令牌失败'; if (message.includes('数据库中无效')) { throw new Error('检测到你可能在其他设备上激活,请重新在此设备上激活。'); } throw new Error('刷新令牌失败,请重新激活'); } } else { if (errorMessage.includes('无效的令牌')) { throw new Error(`凭证无效,请重新激活: ${errorMessage}`); } throw new Error(`认证失败: ${errorMessage || '未知错误'}`); } } if (!response.ok) { const errorData = await response.json(); throw new Error(`请求失败 (${response.status}): ${errorData.error || response.statusText}`); } return response.json(); } function showUsagePanel() { const overlay = document.createElement('div'); overlay.id = 'usage-panel-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 10001; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(8px); `; const modal = document.createElement('div'); modal.style.cssText = ` background: linear-gradient(145deg, #f9fafb, #f3f4f6); padding: 32px 40px; border-radius: 20px; width: 480px; max-width: 90%; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); transform: scale(0.9) translateY(20px); opacity: 0; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); position: relative; border: 1px solid rgba(255, 255, 255, 0.2); display: flex; flex-direction: column; `; const closeModal = () => { modal.style.transform = 'scale(0.9) translateY(20px)'; modal.style.opacity = '0'; overlay.style.opacity = '0'; setTimeout(() => { if (document.body.contains(overlay)) { document.body.removeChild(overlay); } }, 400); }; const closeButton = document.createElement('button'); closeButton.innerHTML = ` `; closeButton.style.cssText = ` position: absolute; top: 18px; right: 18px; background: #e5e7eb; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; color: #6b7280; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; `; closeButton.onmouseover = () => { closeButton.style.transform = 'rotate(90deg) scale(1.1)'; closeButton.style.backgroundColor = '#d1d5db'; }; closeButton.onmouseout = () => { closeButton.style.transform = 'rotate(0deg) scale(1)'; closeButton.style.backgroundColor = '#e5e7eb'; }; closeButton.onclick = closeModal; const title = document.createElement('h2'); title.innerHTML = ` 用量状态 `; title.style.cssText = 'margin-top: 0; margin-bottom: 30px; text-align: center; color: #1f2937; font-size: 24px; font-weight: 700;'; const contentArea = document.createElement('div'); contentArea.style.cssText = ` min-height: 180px; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; `; const spinnerStyle = document.createElement('style'); spinnerStyle.textContent = `@keyframes usage-spinner { to { transform: rotate(360deg); } }`; document.head.appendChild(spinnerStyle); contentArea.innerHTML = `
`; modal.appendChild(closeButton); modal.appendChild(title); modal.appendChild(contentArea); overlay.appendChild(modal); document.body.appendChild(overlay); requestAnimationFrame(() => { overlay.style.opacity = '1'; modal.style.opacity = '1'; modal.style.transform = 'scale(1) translateY(0)'; }); overlay.onclick = (e) => { if (e.target === overlay) closeModal(); }; return { contentArea, closeModal }; } function populateUsagePanel(contentArea, usageData, closeModal) { const { expires_at, total_queries, total_query_limit, daily_queries, daily_query_limit } = usageData; contentArea.innerHTML = ''; contentArea.style.alignItems = 'stretch'; contentArea.style.justifyContent = 'flex-start'; contentArea.style.flexDirection = 'column'; const createUsageBar = (label, used, limit, color) => { const container = document.createElement('div'); container.style.marginBottom = '25px'; const labelElement = document.createElement('div'); labelElement.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 15px; color: #374151;'; const labelText = document.createElement('span'); labelText.textContent = label; labelText.style.fontWeight = '600'; const usageText = document.createElement('span'); usageText.style.cssText = `font-weight: 700; color: #374151; font-family: Microsoft YaHei; font-size: 16px;`; usageText.textContent = `${used.toLocaleString()} / ${limit.toLocaleString()}`; labelElement.appendChild(labelText); labelElement.appendChild(usageText); const progressBarBg = document.createElement('div'); progressBarBg.style.cssText = 'height: 12px; background-color: #e5e7eb; border-radius: 6px; overflow: hidden; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);'; const progressBarFill = document.createElement('div'); const percentage = limit > 0 ? (used / limit) * 100 : 0; progressBarFill.style.cssText = ` height: 100%; width: 0%; background: ${color}; border-radius: 6px; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); `; progressBarBg.appendChild(progressBarFill); container.appendChild(labelElement); container.appendChild(progressBarBg); setTimeout(() => { progressBarFill.style.width = `${percentage}%`; }, 100); return container; }; const dailyUsageBar = createUsageBar('今日已用额度', daily_queries, daily_query_limit, 'linear-gradient(90deg, #5eead4, #3b82f6)'); const totalUsageBar = createUsageBar('总剩余额度', total_queries, total_query_limit, 'linear-gradient(90deg, #f87171, #ec4899)'); const expiryContainer = document.createElement('div'); expiryContainer.style.cssText = ` margin-top: 10px; padding: 15px; text-align: center; background-color: #eef2ff; border: 1px solid #c7d2fe; border-radius: 12px; `; const expiryLabel = document.createElement('span'); expiryLabel.textContent = '授权到期时间: '; expiryLabel.style.color = '#4338ca'; expiryLabel.style.fontWeight = '600'; const expiryDate = document.createElement('span'); expiryDate.textContent = expires_at ? new Date(expires_at * 1000).toLocaleString('zh-CN', { hour12: false }) : 'N/A'; expiryDate.style.fontWeight = '700'; expiryDate.style.color = '#4f46e5'; expiryContainer.appendChild(expiryLabel); expiryContainer.appendChild(expiryDate); const actionsContainer = document.createElement('div'); actionsContainer.style.cssText = 'text-align: center; margin-top: 25px; margin-bottom: 10px;'; const renewButton = document.createElement('button'); renewButton.textContent = '续费 / 激活'; renewButton.style.cssText = ` padding: 12px 28px; border: none; border-radius: 10px; background: linear-gradient(145deg, #4f46e5, #3b82f6); color: white; cursor: pointer; font-size: 16px; font-weight: bold; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 15px rgba(79, 70, 229, 0.25); `; renewButton.onmouseover = () => { renewButton.style.transform = 'translateY(-3px)'; renewButton.style.boxShadow = '0 7px 20px rgba(79, 70, 229, 0.35)'; }; renewButton.onmouseout = () => { renewButton.style.transform = 'translateY(0)'; renewButton.style.boxShadow = '0 4px 15px rgba(79, 70, 229, 0.25)'; }; renewButton.onclick = () => { if (closeModal) { closeModal(); setTimeout(() => { promptActivationCode(); }, 300); } }; actionsContainer.appendChild(renewButton); const announcementContainer = document.createElement('div'); announcementContainer.style.cssText = ` margin-top: 25px; margin-bottom: 10px; padding: 15px 20px; text-align: left; background-color: #fffbeb; border: 1px solid #fde68a; border-radius: 12px; font-size: 14px; line-height: 1.7; color: #78350f; `; const announcementTitle = document.createElement('h4'); announcementTitle.innerHTML = '📢 额度规则调整'; announcementTitle.style.cssText = 'margin-top: 0; margin-bottom: 12px; color: #b45309; font-weight: bold; font-size: 16px;'; const announcementBody = document.createElement('div'); announcementBody.style.cssText = 'margin: 0;'; announcementBody.innerHTML = `
1. 总查询额度调整
2. 每日查询限额

为保障系统稳定,所有用户每日上限统一为 1,000 题,无任何例外。该额度足以满足绝大多数使用场景。

3. 续费与额度规则
`; announcementContainer.appendChild(announcementTitle); announcementContainer.appendChild(announcementBody); contentArea.appendChild(dailyUsageBar); contentArea.appendChild(totalUsageBar); contentArea.appendChild(expiryContainer); contentArea.appendChild(actionsContainer); contentArea.appendChild(announcementContainer); } async function checkUsage() { if (!(await checkAccountConsistency())) { console.warn("[操作中止] 因账号不一致,已取消检查用量。"); return; } const { contentArea, closeModal } = showUsagePanel(); try { const data = await authedFetch('checkUsage', {}); if (data.success) { populateUsagePanel(contentArea, data, closeModal); } else { throw new Error(data.error || '获取用量失败'); } } catch (error) { console.error('检查用量失败:', error); if (error.message.includes('激活')) { closeModal(); setTimeout(promptActivationCode, 300); } else { contentArea.innerHTML = `
获取用量失败:${error.message}
`; } } } let taskNoticesCache = { groupId: null, data: null, timestamp: null, CACHE_DURATION: 5 * 60 * 1000 }; const CONTRIBUTED_ASSIGNMENTS_KEY = 'xiaoya_contributed_assignments'; const CONTRIBUTION_RESCAN_THRESHOLD = 7 * 24 * 60 * 60 * 1000; function getContributedAssignmentsData() { try { const storedData = localStorage.getItem(CONTRIBUTED_ASSIGNMENTS_KEY); if (!storedData) return {}; const parsedData = JSON.parse(storedData); if (Object.values(parsedData).some(val => typeof val === 'number')) { console.log('[贡献数据迁移] 检测到旧的课程级冷却数据,将清空以使用新的作业级冷却机制。'); RuntimePatcher.blessedRemoveItem(localStorage, CONTRIBUTED_ASSIGNMENTS_KEY); return {}; } return (typeof parsedData === 'object' && parsedData !== null) ? parsedData : {}; } catch (error) { console.error('读取已贡献作业数据失败,将重置:', error); RuntimePatcher.blessedRemoveItem(localStorage, CONTRIBUTED_ASSIGNMENTS_KEY); return {}; } } function markAssignmentAsContributed(groupId, nodeId) { if (!groupId || !nodeId) return; const contributedData = getContributedAssignmentsData(); const groupIdStr = groupId.toString(); const nodeIdStr = nodeId.toString(); if (!contributedData[groupIdStr]) { contributedData[groupIdStr] = {}; } contributedData[groupIdStr][nodeIdStr] = Date.now(); localStorage.setItem(CONTRIBUTED_ASSIGNMENTS_KEY, JSON.stringify(contributedData)); console.log(`[本地记录] 作业 (课程 ${groupId}, 节点 ${nodeId}) 的贡献时间戳已更新。`); } async function checkAccountConsistency() { const boundUserId = localStorage.getItem('xiaoya_bound_user_id'); if (!boundUserId) { return true; } const token = getToken(); if (!token) { showNotification('无法获取小雅 Token,请刷新页面或重新登录。', { type: 'error' }); return false; } const currentUserInfo = await getCurrentUserInfo(token); if (!currentUserInfo || !currentUserInfo.id) { showNotification('无法获取当前小雅用户信息,请刷新或重新登录。', { type: 'error' }); return false; } if (currentUserInfo.id.toString() !== boundUserId) { showNotification( '检测到账号不一致!当前操作需要使用激活时绑定的账号。', { type: 'error', duration: 8000 } ); const confirmed = await showConfirmNotification( '脚本检测到当前登录的小雅账号与激活脚本时使用的账号不一致。是否要清除当前激活信息,以便使用新账号重新激活?(你的激活码依旧有效)', { animation: 'scale', title: '账号不一致警告', confirmText: '清除并重新激活', cancelText: '取消操作' } ); if (confirmed) { RuntimePatcher.blessedRemoveItem(localStorage, 'xiaoya_access_token'); RuntimePatcher.blessedRemoveItem(localStorage, 'xiaoya_refresh_token'); RuntimePatcher.blessedRemoveItem(localStorage, 'xiaoya_bound_user_id'); setTimeout(() => promptActivationCode(), 300); } return false; } return true; } async function getTaskNotices(groupId) { const now = Date.now(); if ( taskNoticesCache.groupId === groupId && taskNoticesCache.data && (now - taskNoticesCache.timestamp) < taskNoticesCache.CACHE_DURATION ) { return taskNoticesCache.data; } try { const response = await fetch( `${window.location.origin}/api/jx-stat/group/task/queryTaskNotices?group_id=${groupId}&role=1`, { headers: { 'authorization': `Bearer ${getToken()}`, 'content-type': 'application/json; charset=utf-8' } } ); const data = await response.json(); if (!data.success) { throw new Error('获取作业信息失败'); } taskNoticesCache = { groupId, data: data.data, timestamp: now, CACHE_DURATION: taskNoticesCache.CACHE_DURATION }; return data.data; } catch (error) { console.error('获取任务信息失败:', error); return null; } } async function checkAssignmentStatus(groupId, nodeId) { try { const data = await getTaskNotices(groupId); if (!data) return null; const tasks = data.student_tasks || []; const task = tasks.find(t => t.node_id === nodeId); if (task) { const endTime = new Date(task.end_time); const now = new Date(); const isExpired = now > endTime; const isCompleted = task.finish === 2; return { isExpired, isCompleted, canSubmitAfterExpired: task.is_allow_after_submitted, endTime, status: isCompleted ? '已完成' : (isExpired ? '已截止' : '进行中') }; } throw new Error('未找到作业信息'); } catch (error) { console.error('检查作业状态失败:', error); return null; } } async function isTaskPage() { const groupId = getGroupIDFromUrl(window.location.href); const nodeId = getNodeIDFromUrl(window.location.href); if (!groupId || !nodeId) { return false; } const taskData = await getTaskNotices(groupId); if (!taskData || !taskData.student_tasks) { return false; } const currentTask = taskData.student_tasks.find(task => task.node_id === nodeId); if (!currentTask) { return false; } const validTaskTypes = [2, 3, 4, 5]; return validTaskTypes.includes(currentTask.task_type); } async function getAnswerRecordId(nodeId, groupId, token) { try { const status = await checkAssignmentStatus(groupId, nodeId); if (status) { if (status.isCompleted) { showNotification(`该作业已完成,将不会获取答题记录,仅可查看答案。`, { type: 'warning', keywords: ['已完成'], animation: 'scale' }); return null; } if (status.isExpired) { if (!status.canSubmitAfterExpired) { showNotification(`作业已于 ${status.endTime.toLocaleString()} 截止,且不允许补交,仅可查看答案。`, { type: 'warning', keywords: ['截止', '不允许补交'], animation: 'fadeSlide' }); return null; } showNotification(`作业已于 ${status.endTime.toLocaleString()} 截止,但允许补交。`, { type: 'info', keywords: ['截止', '允许补交'], animation: 'slideRight' }); } } } catch (error) { console.warn("检查作业状态时发生错误,将继续尝试获取记录ID:", error); } const url = `${window.location.origin}/api/jx-iresource/survey/course/task/flow/v2?node_id=${nodeId}&group_id=${groupId}`; console.log('[答题记录] 正在请求任务流程信息:', url); try { const response = await fetch(url, { method: 'GET', headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=utf-8' }, credentials: 'include' }); if (!response.ok) { let errorMsg = `获取答题记录ID失败,服务器状态: ${response.status}`; try { const errorData = await response.json(); errorMsg = errorData.message || errorMsg; } catch (e) { } throw new Error(errorMsg); } const data = await response.json(); if (data.success && data.data) { let recordId = null; if (data.data.task_flow_record && Array.isArray(data.data.task_flow_record) && data.data.task_flow_record.length > 0) { const record = data.data.task_flow_record[0]; if (record && record.answer_record_id) { recordId = record.answer_record_id; console.log(`[答题记录] 从 task_flow_record 成功获取 answer_record_id: ${recordId}`); return recordId; } } if (!recordId && data.data.task_flow_template && Array.isArray(data.data.task_flow_template) && data.data.task_flow_template.length > 0) { const template = data.data.task_flow_template[0]; if (template && template.answer_record_id) { recordId = template.answer_record_id; console.log(`[答题记录] 从 task_flow_template (兼容模式) 成功获取 answer_record_id: ${recordId}`); return recordId; } } } throw new Error('未找到有效的答题记录。请先进入该作业的答题页面以生成它,然后再返回此页面重试。'); } catch (error) { console.error('获取 answer_record_id 时发生错误:', error); throw error; } } async function getAndStoreAnswers() { if (!(await isTaskPage())) { showNotification('当前不是有效的作业/测验页面,或者脚本无法识别。', { type: 'warning' }); return false; } const token = getToken(); if (!token) { showNotification('无法获取token,请确保已登录。', { type: 'error' }); return false; } if (!(await checkAccountConsistency())) { console.warn("[操作中止] 因账号不一致,已取消获取答案。"); return false; } const currentUrl = window.location.href; const node_id = getNodeIDFromUrl(currentUrl); const group_id = getGroupIDFromUrl(currentUrl); if (!node_id || !group_id) { showNotification('无法获取必要参数,请确保在正确的页面。', { type: 'error' }); return false; } const progress = createProgressBar(); progress.show(); let overallSuccess = false; let hitCount = 0; let missCount = 0; let totalQueryableQuestions = 0; try { progress.update(0, 100, '正在获取试卷结构', '%'); const resourceResponse = await fetch(`${window.location.origin}/api/jx-iresource/resource/queryResource/v3?node_id=${node_id}`, { headers: { 'authorization': `Bearer ${token}` }, credentials: 'include' }); const resourceData = await resourceResponse.json(); if (!resourceData.success || !resourceData.data || !resourceData.data.resource) { throw new Error('获取试卷资源失败: ' + (resourceData.message || '返回数据结构不正确')); } progress.update(5, 100, '试卷结构获取成功', '%'); const paperId = resourceData.data.resource.id; const assignmentTitle = resourceData.data.resource.title || '作业答案'; const paperDescription = resourceData.data.resource.description || null; if (paperDescription) { localStorage.setItem('paperDescription', paperDescription); console.log('[全局上下文] 已保存作业头部描述信息。'); } else { RuntimePatcher.blessedRemoveItem(localStorage, 'paperDescription'); } let questionsFromResource = JSON.parse(JSON.stringify(resourceData.data.resource.questions || [])); progress.update(7, 100, '正在获取答题记录', '%'); const recordId = await getAnswerRecordId(node_id, group_id, token); localStorage.setItem('recordId', recordId || ''); progress.update(10, 100, '答题记录获取成功', '%'); localStorage.setItem('groupId', group_id); localStorage.setItem('paperId', paperId); localStorage.setItem('assignmentTitle', assignmentTitle); function mergeAnswerIntoQuestion(question, detailedQuestionInfo) { if (detailedQuestionInfo.title && detailedQuestionInfo.title !== '{}' && detailedQuestionInfo.title !== question.title) { question.title = detailedQuestionInfo.title; } if (!Array.isArray(question.answer_items) || !Array.isArray(detailedQuestionInfo.answer_items)) { console.warn(`问题 ${question.id}: 原始题目或数据库答案的 answer_items 格式不正确,无法合并。`); return; } switch (question.type) { case 1: case 2: { const valueToAnswerInfoMap = new Map(); detailedQuestionInfo.answer_items.forEach(apiItem => { const identifier = getCanonicalContent(apiItem.value); if (identifier) { valueToAnswerInfoMap.set(identifier, { answer_checked: apiItem.answer_checked }); } }); question.answer_items.forEach(qItem => { const identifier = getCanonicalContent(qItem.value); const answerInfo = valueToAnswerInfoMap.get(identifier); if (answerInfo) { qItem.answer_checked = answerInfo.answer_checked; } else { qItem.answer_checked = 1; console.warn(`问题 ${question.id} (选择题): 无法根据内容匹配选项。`, qItem.value); } }); break; } case 5: { const dbCorrectAnswer = detailedQuestionInfo.answer_items.find(item => item.answer_checked === 2); if (!dbCorrectAnswer) { console.warn(`问题 ${question.id} (判断题): 题库中未找到正确答案。`); break; } const isDbAnswerTrue = dbCorrectAnswer.value === 'true'; question.answer_items.forEach(qItem => { const pageOptionText = getCanonicalContent(qItem.value) || parseRichTextToPlainText(qItem.value); const isPageOptionTrue = pageOptionText.includes('正确') || pageOptionText.toLowerCase().includes('true'); if (isDbAnswerTrue === isPageOptionTrue) { qItem.answer_checked = 2; } else { qItem.answer_checked = 1; } }); break; } case 4: { if (question.answer_items.length !== detailedQuestionInfo.answer_items.length) { console.warn(`问题 ${question.id} (填空题): 原始题目与答案的空的数量不匹配,可能导致答案错位。原始: ${question.answer_items.length}, 答案: ${detailedQuestionInfo.answer_items.length}`); } const minLength = Math.min(question.answer_items.length, detailedQuestionInfo.answer_items.length); for (let i = 0; i < minLength; i++) { if (detailedQuestionInfo.answer_items[i] && detailedQuestionInfo.answer_items[i].answer !== undefined) { question.answer_items[i].answer = detailedQuestionInfo.answer_items[i].answer; } } break; } case 12: case 13: { if (question.type === 12) { const valueToAnswerInfoMap = new Map(); detailedQuestionInfo.answer_items.forEach(apiItem => { const identifier = getCanonicalContent(apiItem.value); if (identifier) { valueToAnswerInfoMap.set(identifier, { answer: apiItem.answer }); } }); question.answer_items.forEach(qItem => { const identifier = getCanonicalContent(qItem.value); const answerInfo = valueToAnswerInfoMap.get(identifier); if (answerInfo && answerInfo.answer !== null && answerInfo.answer !== undefined) { qItem.answer = answerInfo.answer; } }); } else { const currentOptionContentToIdMap = new Map(); question.answer_items.forEach(item => { if (item.is_target_opt) { const identifier = getCanonicalContent(item.value); if (identifier) { currentOptionContentToIdMap.set(identifier, item.id); } } }); const dbStemValueToAnswerContentMap = new Map(); detailedQuestionInfo.answer_items.forEach(apiItem => { if (!apiItem.is_target_opt) { const keyIdentifier = getCanonicalContent(apiItem.value); const valueIdentifier = getCanonicalContent(apiItem.answer); if (keyIdentifier) { dbStemValueToAnswerContentMap.set(keyIdentifier, valueIdentifier); } } }); question.answer_items.forEach(qItem => { if (!qItem.is_target_opt) { const stemIdentifier = getCanonicalContent(qItem.value); const correctOptionIdentifier = dbStemValueToAnswerContentMap.get(stemIdentifier); if (correctOptionIdentifier) { const currentCorrectOptionId = currentOptionContentToIdMap.get(correctOptionIdentifier); if (currentCorrectOptionId) { qItem.answer = currentCorrectOptionId; } else { console.warn(`问题 ${question.id} (匹配题): 找到了答案内容 "${correctOptionIdentifier}",但在当前页面选项中找不到匹配项。`); } } } }); } break; } case 6: case 10: { if (detailedQuestionInfo.answer_items?.[0]?.answer !== null && detailedQuestionInfo.answer_items?.[0]?.answer !== undefined) { let rawAnswer = detailedQuestionInfo.answer_items[0].answer; let finalAnswerObject = deepParseJsonString(rawAnswer); let finalAnswerString = (typeof finalAnswerObject === 'object') ? JSON.stringify(finalAnswerObject) : String(finalAnswerObject); if (!question.answer_items || question.answer_items.length === 0) { question.answer_items = [{ answer: finalAnswerString }]; } else { question.answer_items[0].answer = finalAnswerString; } } if (question.type === 10 && detailedQuestionInfo.program_setting) { question.program_setting = detailedQuestionInfo.program_setting; } break; } default: console.log(`问题 ${question.id}: 类型 ${question.type} 暂无特殊答案处理逻辑。`); break; } } const questionsToQuery = []; const SUPPORTED_QUERY_TYPES = [1, 2, 4, 5, 6, 10, 12, 13]; const processQuestionForQuery = (q) => { if (!q) return; if (q.type === 9) { if (q.subQuestions) { q.subQuestions.forEach(processQuestionForQuery); } } else if (SUPPORTED_QUERY_TYPES.includes(q.type)) { const hash = generateContentHash(q); if (hash) { questionsToQuery.push({ question_id: q.id, content_hash: hash, paper_id: paperId, group_id: group_id }); } else { console.warn(`[答案获取] 无法为题目 ${q.id} 生成哈希,跳过查询。`); } } else { console.warn(`[答案获取] 跳过不支持的题型 ${q.id} (类型: ${q.type})`); } }; questionsFromResource.forEach(processQuestionForQuery); totalQueryableQuestions = questionsToQuery.length; if (totalQueryableQuestions === 0) { throw new Error('试卷中没有支持查询的题目'); } const chunkSize = 30; let allAggregatedAnswers = []; progress.update(10, 100, `分批请求答案 (共 ${totalQueryableQuestions} 题)...`, '%'); for (let i = 0; i < questionsToQuery.length; i += chunkSize) { const chunk = questionsToQuery.slice(i, i + chunkSize); const currentProgress = 10 + (i / questionsToQuery.length) * 80; progress.update(currentProgress, 100, `请求第 ${Math.floor(i / chunkSize) + 1} 批答案...`, '%'); const batchResult = await authedFetch('queryAllAnswers', { questionsToQuery: chunk }); if (!batchResult.success || !Array.isArray(batchResult.allAnswers)) { throw new Error(`获取批次答案失败: ${batchResult.error || '后端返回数据格式不正确'}`); } allAggregatedAnswers.push(...batchResult.allAnswers); } progress.update(90, 100, `所有批次请求成功,处理数据...`, '%'); const allAnswersMap = new Map(); hitCount = 0; allAggregatedAnswers.forEach(item => { if (!item || !item.result) { console.warn(`获取问题 ${item?.question_id} 答案失败: 无效的返回项`); return; } const questionData = item.result; if (questionData && questionData.type) { hitCount++; allAnswersMap.set(item.question_id, questionData); } else { console.warn(`获取问题 ${item?.question_id} 答案失败:`, questionData.error || '无法识别的数据格式或未找到答案'); } }); missCount = totalQueryableQuestions - hitCount; progress.update(95, 100, '正在合并答案...', '%'); questionsFromResource.forEach(question => { const detailedQuestionInfo = allAnswersMap.get(question.id); if (detailedQuestionInfo) { mergeAnswerIntoQuestion(question, detailedQuestionInfo); } if (question.type === 9 && question.subQuestions) { question.subQuestions.forEach(subQuestion => { const detailedSubQuestionInfo = allAnswersMap.get(subQuestion.id); if (detailedSubQuestionInfo) { mergeAnswerIntoQuestion(subQuestion, detailedSubQuestionInfo); } }); } }); localStorage.setItem('answerData', JSON.stringify(questionsFromResource)); progress.update(100, 100, '所有答案信息获取完成', '!'); overallSuccess = true; } catch (error) { console.error('获取或处理答案失败:', error); const errorMessage = error.message.toLowerCase(); if (errorMessage.includes('欺诈行为警告')) { showNotification('检测到异常操作,你的授权已被吊销,请重新激活。', { type: 'error', duration: 8000, animation: 'scale' }); RuntimePatcher.blessedRemoveItem(localStorage, 'xiaoya_access_token'); RuntimePatcher.blessedRemoveItem(localStorage, 'xiaoya_refresh_token'); setTimeout(promptActivationCode, 1000); } else if (errorMessage.includes('激活')) { showNotification('你的凭证已失效或需要激活,请操作...', { type: 'warning', duration: 5000, animation: 'scale' }); setTimeout(promptActivationCode, 500); } else { showNotification(`获取答案数据失败:${error.message}`, { type: 'error' }); } overallSuccess = false; } finally { progress.hide(); if (overallSuccess) { let message; let type; let keywords = [String(hitCount), String(missCount), String(totalQueryableQuestions)]; if (hitCount === totalQueryableQuestions && totalQueryableQuestions > 0) { message = `答案获取成功!题库精准命中全部 ${totalQueryableQuestions} 道题!`; type = 'success'; } else if (hitCount > 0) { message = `答案获取成功!共命中 ${hitCount} 道,未命中 ${missCount} 道。`; type = 'success'; keywords.push('命中', '未命中'); } else { message = `答案获取完成,但题库暂无收录 (共查询 ${totalQueryableQuestions} 道题)。`; type = 'warning'; keywords.push('暂无收录'); } showNotification(message, { type: type, keywords: keywords, animation: 'slideRight', duration: 8000 }); } } return overallSuccess; } const SUPPORTED_CONTRIBUTION_TYPES = [1, 2, 4, 5, 6, 10, 12, 13]; function hasValidAnswer_frontEnd(questionData) { if (!questionData || !SUPPORTED_CONTRIBUTION_TYPES.includes(questionData.type)) { return false; } if (!Array.isArray(questionData.answer_items)) { return false; } switch (questionData.type) { case 1: case 2: case 5: return questionData.answer_items.some(item => item.answer_checked === 2); case 4: return questionData.answer_items.some(item => { const answer = item.answer; if (answer === null || answer === undefined || answer === '' || answer === '{}') return false; try { const parsed = JSON.parse(answer); if (parsed.blocks && parsed.blocks.length === 1 && parsed.blocks[0].text === '') { return false; } } catch (e) { } return true; }); case 12: return questionData.answer_items.length > 0 && questionData.answer_items.every( item => item.answer !== null && item.answer !== undefined && item.answer !== '' ); case 13: return questionData.answer_items.some( item => !item.is_target_opt && item.answer !== null && item.answer !== undefined && item.answer !== '' ); case 6: case 10: { if (questionData.answer_items.length === 0) return false; const answer = questionData.answer_items[0]?.answer; if (answer === null || answer === undefined || answer === '') return false; try { const parsed = JSON.parse(answer); if (parsed.blocks && Array.isArray(parsed.blocks)) { if (parsed.blocks.length === 0) return false; if (parsed.blocks.length === 1 && parsed.blocks[0].text === '') { return parsed.blocks[0].type === 'atomic'; } } } catch (e) { } return true; } default: return false; } } async function contributeSingleAssignment(groupId, nodeId) { const token = getToken(); if (!token) return { success: false, error: '无法获取token' }; try { const resourceResponse = await fetch(`${window.location.origin}/api/jx-iresource/resource/queryResource/v3?node_id=${nodeId}`, { headers: { 'authorization': `Bearer ${token}` } }); const resourceData = await resourceResponse.json(); if (!resourceData.success) return { success: false, error: '获取试卷资源失败' }; const paperId = resourceData.data?.resource?.id; if (!paperId) return { success: false, error: '无法从资源中获取 paperId' }; const answerSheetResponse = await fetch(`${window.location.origin}/api/jx-iresource/survey/course/queryStuPaper/v2?paper_id=${paperId}&group_id=${groupId}&node_id=${nodeId}`, { headers: { 'authorization': `Bearer ${token}` } }); const answerSheetData = await answerSheetResponse.json(); if (!answerSheetData.success || !answerSheetData.data || !answerSheetData.data.questions || answerSheetData.data.questions.length === 0) { return { success: false, error: '获取答案数据失败: ' + (answerSheetData.message || '无题目信息') }; } const flattenQuestions = (questionList) => { let flatList = []; if (!Array.isArray(questionList)) { console.warn("[flattenQuestions] 输入不是一个数组:", questionList); return flatList; } questionList.forEach(q => { if (!q) return; flatList.push(q); if (q.type === 9 && Array.isArray(q.subQuestions)) { flatList.push(...flattenQuestions(q.subQuestions)); } }); return flatList; }; const clonedQuestions = JSON.parse(JSON.stringify(answerSheetData.data.questions)); const allClonedQuestionsMap = new Map(flattenQuestions(clonedQuestions).map(q => [q.id, q])); const originalQuestionsData = answerSheetData.data.questions; const allOriginalQuestionsMap = new Map(flattenQuestions(originalQuestionsData).map(q => [q.id, q])); const studentCorrectAnswers = new Map(); if (answerSheetData.data.answer_record && answerSheetData.data.answer_record.answers) { answerSheetData.data.answer_record.answers.forEach(ans => { if (ans.correct === 2 || ans.score > 0) { studentCorrectAnswers.set(ans.question_id, ans.answer); } }); } allClonedQuestionsMap.forEach(question => { const studentAnswer = studentCorrectAnswers.get(question.id); if (studentAnswer !== undefined) { console.log(`[贡献] 题目 ${question.id} (类型 ${question.type}) 使用【学生正确作答记录】填充。`); switch (question.type) { case 1: case 5: question.answer_items.forEach(item => { item.answer_checked = (item.id === String(studentAnswer)) ? 2 : 1; }); break; case 2: { let selectedItemIds = []; if (Array.isArray(studentAnswer)) selectedItemIds = studentAnswer.map(String); else if (typeof studentAnswer === 'string') selectedItemIds = studentAnswer.split(',').map(id => id.trim()).filter(id => id); question.answer_items.forEach(item => { item.answer_checked = selectedItemIds.includes(item.id) ? 2 : 1; }); break; } case 4: try { const fillAnswersObject = JSON.parse(studentAnswer); question.answer_items.forEach(item => { if (fillAnswersObject.hasOwnProperty(item.id)) { item.answer = JSON.stringify({ blocks: [{ text: fillAnswersObject[item.id] || '' }] }); } }); } catch (e) { console.warn(`[贡献] 解析学生填空题答案失败 (ID: ${question.id})`, e); } break; case 6: if (question.answer_items && question.answer_items.length > 0) question.answer_items[0].answer = JSON.stringify({ blocks: [{ text: studentAnswer }] }); break; case 10: try { const parsedAnswer = JSON.parse(studentAnswer); if (!question.program_setting) question.program_setting = {}; question.program_setting.code_answer = parsedAnswer.code || studentAnswer; } catch (e) { if (!question.program_setting) question.program_setting = {}; question.program_setting.code_answer = studentAnswer; } break; case 12: { let sortedItemIds = []; if (Array.isArray(studentAnswer)) sortedItemIds = studentAnswer.map(String); else if (typeof studentAnswer === 'string') sortedItemIds = studentAnswer.split(',').map(id => id.trim()).filter(id => id); question.answer_items.forEach(item => { const order = sortedItemIds.indexOf(item.id); item.answer = (order !== -1) ? (order + 1).toString() : ''; }); break; } case 13: try { const matchObject = (typeof studentAnswer === 'string' ? JSON.parse(studentAnswer) : studentAnswer)[0] || (typeof studentAnswer === 'string' ? JSON.parse(studentAnswer) : studentAnswer); const optionIdToValueMap = new Map(); question.answer_items.forEach(item => { if (item.is_target_opt) { optionIdToValueMap.set(item.id, item.value); } }); question.answer_items.forEach(item => { if (!item.is_target_opt && matchObject.hasOwnProperty(item.id)) { const matchedOptionId = matchObject[item.id]; if (optionIdToValueMap.has(matchedOptionId)) { item.answer = optionIdToValueMap.get(matchedOptionId); } } }); } catch (e) { console.warn(`[贡献] 解析学生匹配题答案失败 (ID: ${question.id})`, e); } break; } } else { const originalQuestion = allOriginalQuestionsMap.get(question.id); if (originalQuestion && hasValidAnswer_frontEnd(originalQuestion)) { console.log(`[贡献] 题目 ${question.id} (类型 ${question.type}) 无学生作答记录,但使用【原始官方答案】。`); } else { console.log(`[贡献] 题目 ${question.id} (类型 ${question.type}) 无任何有效答案源,将在后续被过滤。`); } } }); const contributedQuestions = Array.from(allClonedQuestionsMap.values()).filter(q => hasValidAnswer_frontEnd(q)); console.log(`[贡献] 准备贡献 ${contributedQuestions.length} 道高质量题目。`); if (contributedQuestions.length === 0) { return { success: false, error: '未解析到任何有效答案' }; } const finalContributedData = contributedQuestions.map(q => { const hash = generateContentHash(q); if (!hash) { console.warn(`[贡献] 无法为题目 ${q.id} 生成哈希,跳过贡献。`); return null; } return { question_id: q.id, paper_id: q.paper_id, content_hash: hash, answer_data: q }; }).filter(Boolean); if (finalContributedData.length === 0) { return { success: false, error: '所有可贡献题目都无法生成有效哈希' }; } const response = await authedFetch('contributeAnswers', { contributedQuestions: finalContributedData }); if (response.success) { markAssignmentAsContributed(groupId, nodeId); return { success: true, message: response.message }; } else { return { success: false, error: response.error || '上传贡献失败' }; } } catch (error) { console.error(`贡献作业 (nodeId: ${nodeId}) 时出错:`, error); return { success: false, error: error.message }; } } async function asyncPool(poolLimit, array, iteratorFn) { const ret = []; const executing = []; for (const item of array) { const p = Promise.resolve().then(() => iteratorFn(item, array)); ret.push(p); if (poolLimit <= array.length) { const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); if (executing.length >= poolLimit) { await Promise.race(executing); } } } return Promise.all(ret); } async function scanAndContributeCourse(course, isPastCourse = false) { const groupId = course.id; const contributedData = getContributedAssignmentsData(); const now = Date.now(); try { const tasksData = await getTaskNotices(groupId); if (!tasksData || !tasksData.student_tasks) { console.error(`[后台扫描] 获取课程 "${course.name}" (ID: ${groupId}) 的任务列表失败。`); return { success: 0, failed: 0 }; } const validTaskTypes = [2, 3, 4, 5]; const allAssignments = tasksData.student_tasks.filter(task => validTaskTypes.includes(task.task_type) ); const assignmentsToScan = allAssignments.filter(task => { const lastScanTimestamp = contributedData[groupId.toString()]?.[task.node_id.toString()]; if (!lastScanTimestamp) { return true; } if (isPastCourse) { console.log(`[后台扫描] 作业 (ID: ${task.node_id}) 属于已结束课程且已贡献过,将永久跳过。`); return false; } return now - lastScanTimestamp > CONTRIBUTION_RESCAN_THRESHOLD; }); if (assignmentsToScan.length === 0) { console.log(`[后台扫描] 课程 "${course.name}" (ID: ${groupId}) 中没有需要贡献的新作业。`); return { success: 0, failed: 0 }; } console.log(`[后台扫描] 课程 "${course.name}" (ID: ${groupId}) 中发现 ${assignmentsToScan.length} 个需要处理的作业。`); let successCount = 0; let failCount = 0; const CONCURRENCY_LIMIT = 2; await asyncPool(CONCURRENCY_LIMIT, assignmentsToScan, async (task) => { const result = await contributeSingleAssignment(groupId, task.node_id); if (result.success) { successCount++; } else { if (result.error === '未解析到任何有效答案') { console.log(`[后台扫描] 作业 (ID: ${task.node_id}) 无有效答案可贡献,标记为已检查。`); markAssignmentAsContributed(groupId, task.node_id); } else { failCount++; console.warn(`[后台扫描] 贡献作业 (ID: ${task.node_id}) 失败: ${result.error}`); } } await new Promise(resolve => setTimeout(resolve, 800)); }); return { success: successCount, failed: failCount }; } catch (error) { console.error(`[后台扫描] 处理课程 "${course.name}" (ID: ${groupId}) 时发生严重错误:`, error); return { success: 0, failed: 1 }; } } async function backgroundContributeAllCourses() { if (!autoContributeEnabled) { return false; } if (!(await checkAccountConsistency())) { console.log("[后台扫描] 因账号不一致,已中止全量扫描。"); return false; } const token = getToken(); if (!token) { return false; } ContributionProgressUI.show('正在准备后台贡献任务...'); console.log('[后台扫描] 开始执行全量课程扫描...'); try { const MAX_RETRIES = 3; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { console.log(`[后台扫描] 正在进行用量预检 (尝试 ${attempt}/${MAX_RETRIES})...`); await authedFetch('checkUsage', {}); console.log(`[后台扫描] 用量预检成功。`); break; } catch (error) { console.warn(`[后台扫描] 用量预检尝试 ${attempt} 失败:`, error.message); if (attempt < MAX_RETRIES) { const delay = 1500 * attempt; console.log(`[后台扫描] 将在 ${delay / 1000} 秒后重试...`); await new Promise(resolve => setTimeout(resolve, delay)); } else { console.error(`[后台扫描] 用量预检在 ${MAX_RETRIES} 次尝试后彻底失败,后台贡献任务中止。`); throw error; } } } const fetchCourses = async (timeFlag) => { const url = `${window.location.origin}/api/jx-iresource/group/student/groups?time_flag=${timeFlag}`; const response = await fetch(url, { headers: { 'authorization': `Bearer ${token}` } }); if (!response.ok) throw new Error(`获取课程列表失败 (flag=${timeFlag})`); const data = await response.json(); return data.success ? data.data : []; }; const [currentCourses, pastCourses] = await Promise.all([fetchCourses(1), fetchCourses(3)]); const courseMap = new Map(); currentCourses.forEach(course => course && course.id && courseMap.set(course.id, { ...course, isPast: false })); pastCourses.forEach(course => { if (course && course.id && !courseMap.has(course.id)) { courseMap.set(course.id, { ...course, isPast: true }); } }); const allCourses = Array.from(courseMap.values()); if (allCourses.length === 0) { console.log('[后台扫描] 未获取到任何课程列表,任务结束。'); ContributionProgressUI.complete('未找到任何课程。'); return true; } ContributionProgressUI.show(`后台将检查 ${allCourses.length} 门课程中的新作业...`); showNotification(`后台将为你检查所有课程,寻找可贡献的新答案...`, { type: 'info', duration: 7000 }); let totalNewContributions = 0; for (let i = 0; i < allCourses.length; i++) { const courseInfo = allCourses[i]; console.log(`[后台扫描] [${i + 1}/${allCourses.length}] 正在检查课程: ${courseInfo.name} ${courseInfo.isPast ? '(已结束)' : ''}`); ContributionProgressUI.update(i + 1, allCourses.length, courseInfo.name); const result = await scanAndContributeCourse(courseInfo, courseInfo.isPast); totalNewContributions += result.success; } console.log(`[后台扫描] 全部完成!本次共贡献了 ${totalNewContributions} 个新作业。`); ContributionProgressUI.complete(`扫描完成!感谢你的 ${totalNewContributions} 个新贡献!`); if (totalNewContributions > 0) { showNotification(`后台扫描完成,感谢你为答案库贡献了 ${totalNewContributions} 个新作业!`, { type: 'success', duration: 10000 }); } return true; } catch (error) { const errorMessage = error.message.toLowerCase(); if (errorMessage.includes('激活') || errorMessage.includes('失效') || errorMessage.includes('到期') || errorMessage.includes('欺诈')) { console.error(`[后台扫描] 因授权问题中止: "${error.message}"`); } else { console.error('[后台扫描] 操作失败:', error); } ContributionProgressUI.error(error.message); return false; } } async function getSubmittedAnswers() { if (!(await isTaskPage())) { showNotification('当前不是有效的作业/测验页面,或者脚本无法识别。', { type: 'warning', keywords: ['作业', '测验'], animation: 'scale' }); return; } try { const token = getToken(); if (!token) { showNotification('无法获取token,请确保已登录。', { type: 'error', keywords: ['token', '登录'], animation: 'fadeSlide' }); return; } const currentUrl = window.location.href; const node_id = getNodeIDFromUrl(currentUrl); const group_id = getGroupIDFromUrl(currentUrl); if (!node_id || !group_id) { showNotification('无法获取必要参数,请确保在正确的页面。', { type: 'error', keywords: ['参数'], animation: 'slideRight' }); return; } const progress = createProgressBar(); progress.show(); progress.update(0, 1, '正在获取已提交作业'); const resourceData = await fetch( `${window.location.origin}/api/jx-iresource/resource/queryResource/v3?node_id=${node_id}`, { headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=utf-8' }, credentials: 'include' } ).then(res => res.json()); if (!resourceData.success) { throw new Error('获取试卷资源失败'); } const assignmentTitle = resourceData.data?.resource?.title || '作业答案'; localStorage.setItem('assignmentTitle', assignmentTitle); const paperDescription = resourceData.data?.resource?.description || null; if (paperDescription) { localStorage.setItem('paperDescription', paperDescription); console.log('[全局上下文 - 已提交] 已同步更新作业头部描述信息。'); } else { RuntimePatcher.blessedRemoveItem(localStorage, 'paperDescription'); console.log('[全局上下文 - 已提交] 当前作业无头部描述,已清除旧的缓存。'); } const paper_id = resourceData.data.resource.id; const submittedAnswerResponse = await fetch( `${window.location.origin}/api/jx-iresource/survey/course/queryStuPaper/v2?paper_id=${paper_id}&group_id=${group_id}&node_id=${node_id}`, { headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=utf-8' }, credentials: 'include' } ); const submittedAnswerData = await submittedAnswerResponse.json(); progress.update(1, 1, '已获取提交答案'); if (!submittedAnswerData.success) { throw new Error('获取已提交作业失败'); } if (submittedAnswerData.data && submittedAnswerData.data.answer_record && submittedAnswerData.data.answer_record.answers && submittedAnswerData.data.answer_record.answers.length > 0) { localStorage.setItem('submittedAnswerData', JSON.stringify(submittedAnswerData.data.answer_record.answers)); const questionsData = resourceData.data.resource.questions; const allQuestionsMap = new Map(); questionsData.forEach(q => { allQuestionsMap.set(q.id, q); if (q.type === 9 && q.subQuestions) { q.subQuestions.forEach(sq => { allQuestionsMap.set(sq.id, sq); sq.parent_question_id = q.id; }); } }); submittedAnswerData.data.answer_record.answers.forEach(submittedAnswer => { const questionId = submittedAnswer.question_id; const question = allQuestionsMap.get(questionId); if (question) { if (question.type === 1 || question.type === 5) { const selectedItemId = String(submittedAnswer.answer); if (question.answer_items) { question.answer_items.forEach(item => { item.answer_checked = (item.id === selectedItemId) ? 2 : 1; }); } } else if (question.type === 2) { let selectedItemIds = []; if (Array.isArray(submittedAnswer.answer)) { selectedItemIds = submittedAnswer.answer.map(String); } else if (typeof submittedAnswer.answer === 'string') { if (submittedAnswer.answer.includes(',')) { selectedItemIds = submittedAnswer.answer.split(',').map(id => id.trim()).filter(id => id); } else if (submittedAnswer.answer.length > 0) { selectedItemIds = [submittedAnswer.answer]; } } if (question.answer_items) { question.answer_items.forEach(item => { item.answer_checked = selectedItemIds.includes(item.id) ? 2 : 1; }); } } else if (question.type === 4) { try { const fillAnswersObject = JSON.parse(submittedAnswer.answer); if (question.answer_items && typeof fillAnswersObject === 'object' && fillAnswersObject !== null) { question.answer_items.forEach(item => { if (fillAnswersObject.hasOwnProperty(item.id)) { const plainTextStudentAnswer = fillAnswersObject[item.id]; item.answer = JSON.stringify({ blocks: [{ key: `ans-${item.id}`, text: plainTextStudentAnswer, type: 'unstyled', depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} }], entityMap: {} }); } else { item.answer = JSON.stringify({ blocks: [{ key: `empty-${item.id}`, text: "", type: 'unstyled', depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} }], entityMap: {} }); } }); } } catch (e) { console.error(`解析填空题已提交作业失败 (questionId: ${questionId}):`, e, "Raw answer:", submittedAnswer.answer); if (question.answer_items) { question.answer_items.forEach(item => { item.answer = JSON.stringify({ blocks: [{ key: `error-${item.id}`, text: "", type: 'unstyled', depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} }], entityMap: {} }); }); } } } else if (question.type === 6) { if (question.answer_items && question.answer_items.length > 0) { const plainTextStudentAnswer = submittedAnswer.answer; try { JSON.parse(plainTextStudentAnswer); question.answer_items[0].answer = plainTextStudentAnswer; } catch (err) { question.answer_items[0].answer = JSON.stringify({ blocks: [{ key: `ans-${question.id}`, text: plainTextStudentAnswer, type: 'unstyled', depth: 0, inlineStyleRanges: [], entityRanges: [], data: {} }], entityMap: {} }); } } } else if (question.type === 10) { try { const parsedAnswer = JSON.parse(submittedAnswer.answer); if (parsedAnswer && parsedAnswer.code) { if (!question.program_setting) question.program_setting = {}; question.program_setting.code_answer = parsedAnswer.code; } else if (typeof submittedAnswer.answer === 'string' && !submittedAnswer.answer.startsWith('{')) { if (!question.program_setting) question.program_setting = {}; question.program_setting.code_answer = submittedAnswer.answer; } } catch (e) { if (question.program_setting) { question.program_setting.code_answer = submittedAnswer.answer; } else { question.program_setting = { code_answer: submittedAnswer.answer }; } console.warn(`解析编程题已获取答案可能不是标准JSON (questionId: ${questionId}):`, e, "Raw answer:", submittedAnswer.answer); } } else if (question.type === 12) { let sortedItemIds = []; if (Array.isArray(submittedAnswer.answer)) { sortedItemIds = submittedAnswer.answer.map(String); } else if (typeof submittedAnswer.answer === 'string') { if (submittedAnswer.answer.includes(',')) { sortedItemIds = submittedAnswer.answer.split(',').map(id => id.trim()).filter(id => id); } else if (submittedAnswer.answer.length > 0) { sortedItemIds = [submittedAnswer.answer]; } } if (question.answer_items && sortedItemIds.length > 0) { question.answer_items.forEach(item => { const order = sortedItemIds.indexOf(item.id); if (order !== -1) { item.answer = (order + 1).toString(); } else { item.answer = ''; console.warn(`排序题 (questionId: ${questionId}) 的选项 item.id: ${item.id} 未在提交的答案中找到:`, sortedItemIds); } }); } } else if (question.type === 13) { try { let matchObject = null; const parsedAnswerData = JSON.parse(submittedAnswer.answer); if (Array.isArray(parsedAnswerData) && parsedAnswerData.length > 0 && typeof parsedAnswerData[0] === 'object') { matchObject = parsedAnswerData[0]; } else if (typeof parsedAnswerData === 'object' && !Array.isArray(parsedAnswerData)) { matchObject = parsedAnswerData; } if (matchObject && question.answer_items) { question.answer_items.forEach(item => { if (!item.is_target_opt) { if (matchObject.hasOwnProperty(item.id)) { item.answer = matchObject[item.id]; } else { item.answer = ''; } } }); } } catch (e) { console.error(`解析匹配题已获取答案失败 (questionId: ${questionId}):`, e, "Raw answer:", submittedAnswer.answer); } } } else { console.warn(`在 submittedAnswers 中找到一个答案,但其 question_id (${questionId}) 在 questionsData 或其子问题中均未找到。`); } }); localStorage.setItem('answerData', JSON.stringify(questionsData)); progress.hide(); showNotification('已提交作业获取成功!', { type: 'success', keywords: ['已提交', '答案', '获取'], animation: 'scale' }); return true; } else { progress.hide(); showNotification('未找到已提交的答案,可能尚未提交或无权限查看。', { type: 'warning', keywords: ['未找到', '已提交'], animation: 'fadeSlide' }); return false; } } catch (error) { console.error('获取已提交作业失败:', error); showNotification('获取已提交作业失败:' + (error.message || '未知错误'), { type: 'error', keywords: ['获取', '失败'], animation: 'scale' }); return false; } } async function fillAnswers() { const answerData = JSON.parse(localStorage.getItem('answerData')); const recordId = localStorage.getItem('recordId'); const groupId = localStorage.getItem('groupId'); const paperId = localStorage.getItem('paperId'); if (!answerData || !recordId || !groupId || !paperId) { showNotification('缺少必要数据,请先获取答案或检查作业状态。', { type: 'error', keywords: ['数据', '获取', '检查'], animation: 'scale' }); return; } const token = getToken(); if (!token) { showNotification('无法获取token。', { type: 'error', keywords: ['token'], animation: 'slideRight' }); return; } const progress = createProgressBar(); progress.show(); try { let completedCount = 0; const totalQuestions = answerData.length; const batchSize = 10; for (let i = 0; i < answerData.length; i += batchSize) { const batch = answerData.slice(i, i + batchSize); let localCompletedCount = completedCount; await Promise.all(batch.map(async question => { await submitAnswer(question, recordId, groupId, paperId, token); localCompletedCount++; progress.update(localCompletedCount, totalQuestions); })); completedCount = localCompletedCount; } progress.hide(); showNotification('答案填写完成!页面将于0.5s后刷新。', { type: 'success', keywords: ['答案', '填写', '刷新'], animation: 'slideRight' }); const nodeId = getNodeIDFromUrl(window.location.href); const currentGroupId = getGroupIDFromUrl(window.location.href); if (nodeId && currentGroupId) sessionStorage.setItem(`xiaoya_autofilled_${currentGroupId}_${nodeId}`, 'true'); setTimeout(() => { location.reload(); }, 500); } catch (error) { progress.hide(); console.error('填写答案失败:', error); showNotification('填写答案失败,请查看控制台。', { type: 'error', keywords: ['填写', '失败'], animation: 'scale' }); } } async function submitAnswer(question, recordId, groupId, paperId, token) { let answer; let extAnswer = ''; switch (question.type) { case 1: { answer = [question.answer_items.find(item => item.answer_checked === 2)?.id]; break; } case 2: { answer = question.answer_items.filter(item => item.answer_checked === 2).map(item => item.id); break; } case 4: { const fillObject = {}; question.answer_items.forEach(item => { fillObject[item.id] = parseRichTextToPlainText(item.answer); }); answer = [fillObject]; break; } case 5: { answer = [question.answer_items.find(item => item.answer_checked === 2)?.id]; break; } case 6: { answer = [question.answer_items[0].answer]; break; } case 9: { if (question.subQuestions && question.subQuestions.length > 0) { for (const subQuestion of question.subQuestions) { await submitAnswer(subQuestion, recordId, groupId, paperId, token); } } return; } case 10: { const progSetting = question.program_setting || {}; const answerItem = question.answer_items?.[0]; answer = [{ language: progSetting.language?.[0] || 'c', code: progSetting.code_answer || '', answer_item_id: answerItem?.id || '' }]; break; } case 12: { answer = question.answer_items .sort((a, b) => parseInt(a.answer) - parseInt(b.answer)) .map(item => item.id); break; } case 13: { const matchObject = {}; question.answer_items .filter(item => !item.is_target_opt && item.answer) .forEach(item => { matchObject[item.id] = item.answer; }); if (Object.keys(matchObject).length > 0) { answer = [matchObject]; } else { return; } break; } default: return; } const requestBody = { record_id: recordId, question_id: question.id, answer: answer, ext_answer: extAnswer, group_id: groupId, paper_id: paperId, is_try: 0 }; return fetch(`${window.location.origin}/api/jx-iresource/survey/answer`, { method: 'POST', headers: { 'accept': '*/*', 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=UTF-8' }, body: JSON.stringify(requestBody) }); } const INLINE_STYLE_TAGS = { BOLD: { open: '', close: '' }, ITALIC: { open: '', close: '' }, UNDERLINE: { open: '', close: '' }, STRIKETHROUGH: { open: '', close: '' }, CODE: { open: '', close: '' }, HIGHLIGHT: { open: '', close: '' }, SUBSCRIPT: { open: '', close: '' }, SUPERSCRIPT: { open: '', close: '' } }; const STYLE_WRAP_ORDER = ['SUBSCRIPT', 'SUPERSCRIPT', 'CODE', 'BOLD', 'ITALIC', 'UNDERLINE', 'STRIKETHROUGH', 'HIGHLIGHT']; function escapeHtml(str = '') { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeHtmlAttr(str = '') { return String(str) .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } function wrapWithStyles(html, stylesSet) { if (!stylesSet || stylesSet.size === 0) return html; let wrapped = html; STYLE_WRAP_ORDER.forEach(style => { const normalized = style.toUpperCase(); if (stylesSet.has(normalized) && INLINE_STYLE_TAGS[normalized]) { const { open, close } = INLINE_STYLE_TAGS[normalized]; wrapped = `${open}${wrapped}${close}`; } }); return wrapped; } function getEntityByKey(entityMap, key) { if (!entityMap || key === undefined || key === null) return null; if (Object.prototype.hasOwnProperty.call(entityMap, key)) { return entityMap[key]; } const stringKey = String(key); if (Object.prototype.hasOwnProperty.call(entityMap, stringKey)) { return entityMap[stringKey]; } return null; } function findEntityCoveringRange(entityRanges = [], start, end, entityMap) { for (const range of entityRanges) { if (!range || typeof range.offset !== 'number' || typeof range.length !== 'number' || range.length <= 0) continue; const rangeStart = range.offset; const rangeEnd = range.offset + range.length; if (start >= rangeStart && end <= rangeEnd) { return getEntityByKey(entityMap, range.key); } } return null; } function convertEntityToHtml(entity, rawText) { if (!entity) return escapeHtml(rawText); const entityType = (entity.type || '').toUpperCase(); const data = entity.data || {}; if (entityType === 'INLINETEX' || entityType === 'INLINE_TEX' || entityType === 'TEX') { const tex = data.teX || data.tex || data.value || data.content; if (tex) { return `\\(${escapeHtml(tex)}\\)`; } } else if (entityType === 'BLOCKTEX' || entityType === 'TEXBLOCK' || entityType === 'DISPLAYTEX') { const texBlock = data.teX || data.tex || data.value || data.content; if (texBlock) { return `
\\[${escapeHtml(texBlock)}\\]
`; } } else if (entityType === 'LINK' || entityType === 'HYPERLINK') { const href = data.url || data.href; if (href) { const targetAttr = data.target ? ` target="${escapeHtmlAttr(data.target)}"` : ' target="_blank"'; return `${escapeHtml(rawText) || escapeHtmlAttr(href)}`; } } else if (entityType === 'IMAGE' || entityType === 'IMG') { if (data.src) { const src = escapeHtmlAttr(data.src); const alt = escapeHtmlAttr(data.alt || '内容图片'); return `${alt}`; } } return escapeHtml(rawText); } function buildInlineHtml(block, entityMap) { const text = typeof block.text === 'string' ? block.text : ''; if (!text) return ''; const breakpoints = new Set([0, text.length]); (block.inlineStyleRanges || []).forEach(range => { if (range && typeof range.offset === 'number' && typeof range.length === 'number' && range.length > 0) { breakpoints.add(range.offset); breakpoints.add(range.offset + range.length); } }); (block.entityRanges || []).forEach(range => { if (range && typeof range.offset === 'number' && typeof range.length === 'number' && range.length > 0) { breakpoints.add(range.offset); breakpoints.add(range.offset + range.length); } }); for (let i = 0; i < text.length; i++) { if (text[i] === '\n') { breakpoints.add(i); breakpoints.add(i + 1); } } const sortedBreakpoints = Array.from(breakpoints) .filter(index => index >= 0 && index <= text.length) .sort((a, b) => a - b); let html = ''; for (let i = 0; i < sortedBreakpoints.length - 1; i++) { const start = sortedBreakpoints[i]; const end = sortedBreakpoints[i + 1]; if (start >= end) continue; const segment = text.slice(start, end); if (!segment) continue; if (segment === '\n') { html += '
'; continue; } const styles = new Set(); (block.inlineStyleRanges || []).forEach(range => { if (!range || typeof range.offset !== 'number' || typeof range.length !== 'number' || range.length <= 0) return; const rangeStart = range.offset; const rangeEnd = range.offset + range.length; if (start >= rangeStart && end <= rangeEnd) { styles.add(String(range.style || '').toUpperCase()); } }); const entity = findEntityCoveringRange(block.entityRanges || [], start, end, entityMap); let segmentHtml = entity ? convertEntityToHtml(entity, segment) : escapeHtml(segment); if (segmentHtml && segmentHtml.replace) { segmentHtml = segmentHtml.replace(/ {2}/g, '  '); } segmentHtml = wrapWithStyles(segmentHtml, styles); html += segmentHtml; } return html; } async function renderAtomicBlock(block, aiConfig = {}) { const data = block.data || {}; const normalizedType = (data.type || '').toUpperCase(); if (normalizedType === 'IMAGE' && data.src) { const fileIdMatch = data.src.match(/\/cloud\/file_access\/(\d+)/); if (fileIdMatch && fileIdMatch[1]) { const fileId = fileIdMatch[1]; const imageUrl = `${window.location.origin}/api/jx-oresource/cloud/file_access/${fileId}?random=${Date.now()}`; return `
内容图片
[图片加载失败]
`; } return `
[图片链接格式无法解析]
`; } if (normalizedType === 'AUDIO' && data.data && data.data.quote_id) { const fileId = String(data.data.quote_id); const cacheKey = `audio_url_${fileId}`; let audioUrl = sessionStorage.getItem(cacheKey); if (!audioUrl) { audioUrl = await getAudioUrl(fileId); if (audioUrl) sessionStorage.setItem(cacheKey, audioUrl); } if (audioUrl) { const safeAudioUrl = escapeHtmlAttr(audioUrl); const safeFileId = escapeHtmlAttr(fileId); return `
`; } return `
[音频加载失败]
`; } if (normalizedType === 'VIDEO' && data.data && data.data.video_id) { const videoId = String(data.data.video_id); const cacheKey = `video_urls_${videoId}`; let urls = null; try { urls = JSON.parse(sessionStorage.getItem(cacheKey) || 'null'); } catch (error) { urls = null; } if (!urls) { urls = await getVideoUrl(videoId); if (urls && urls.videoUrl) { sessionStorage.setItem(cacheKey, JSON.stringify(urls)); } } if (urls && urls.videoUrl) { const safeVideoUrl = escapeHtmlAttr(urls.videoUrl); const safeVideoId = escapeHtmlAttr(videoId); let videoHtml = `
`; if (aiConfig.sttEnabled && aiConfig.sttVideoEnabled) { videoHtml += `
`; } videoHtml += `
`; return videoHtml; } return `
[视频加载失败: ${escapeHtml(videoId)}]
`; } if ((normalizedType === 'MATH' || normalizedType === 'TEX' || normalizedType === 'TEXBLOCK' || normalizedType === 'DISPLAYTEX') && (data.teX || data.tex || data.value)) { const texBlock = data.teX || data.tex || data.value; return `
\\[${escapeHtml(texBlock)}\\]
`; } return ''; } async function parseRichTextContentAsync(content) { if (!content || typeof content !== 'string') return content || ''; try { const jsonContent = JSON.parse(content); if (!jsonContent || !Array.isArray(jsonContent.blocks)) { return escapeHtml(content).replace(/\n/g, '
'); } let htmlResult = ''; const aiConfig = JSON.parse(localStorage.getItem('aiConfig') || '{}'); const entityMap = jsonContent.entityMap || {}; let activeListType = null; const closeActiveList = () => { if (activeListType === 'unordered-list-item') { htmlResult += ''; } else if (activeListType === 'ordered-list-item') { htmlResult += ''; } activeListType = null; }; for (const block of jsonContent.blocks) { if (!block) continue; const blockType = block.type || 'unstyled'; if (blockType === 'atomic' && block.data) { closeActiveList(); htmlResult += await renderAtomicBlock(block, aiConfig); continue; } if (blockType === 'unordered-list-item' || blockType === 'ordered-list-item') { if (activeListType !== blockType) { closeActiveList(); htmlResult += blockType === 'ordered-list-item' ? '
    ' : '