// ==UserScript== // @name 超星自行火炮 // @namespace chaoxing-helper // @version 4.0.12 // @author isMobile // @description 超星自行火炮——自动化完成超星视频/音频/文档/答题(选择/简答)任务,不连接题库,使用大模型完成答题 // @license MIT // @icon https://vitejs.dev/logo.svg // @match *://*.chaoxing.com/* // @match *://*.edu.cn/* // @match *://*.nbdlib.cn/* // @match *://*.hnsyu.net/* // @match *://*.gdhkmooc.com/* // @require https://lib.baomitu.com/vue/3.4.31/vue.global.prod.js // @require https://lib.baomitu.com/vue-demi/0.14.7/index.iife.js // @require data:application/javascript,window.Vue%3DVue%3B // @require https://lib.baomitu.com/element-plus/2.7.2/index.full.min.js // @require https://lib.baomitu.com/pinia/2.1.7/pinia.iife.min.js // @require https://lib.baomitu.com/rxjs/7.8.1/rxjs.umd.min.js // @require https://lib.baomitu.com/blueimp-md5/2.19.0/js/md5.min.js // @resource ElementPlusStyle https://lib.baomitu.com/element-plus/2.8.2/index.min.css // @resource ttf https://www.forestpolice.org/ttf/2.0/table.json // @connect localhost // @connect chaoxing.cloud.caqing.top // @connect * // @grant GM_getResourceText // @grant GM_getValue // @grant GM_info // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function(vue, pinia, rxjs, md5, ElementPlus) { 'use strict'; // ==================== 0. 环境钩子与防检测 ==================== const VISIBILITY_EVENTS = [ 'visibilitychange', 'webkitvisibilitychange', 'mozvisibilitychange', 'msvisibilitychange', 'blur', 'pagehide', 'freeze' ]; const patchVisibilityProps = (doc, win) => { if (!doc || !win) return; try { const docProto = win.Document?.prototype; if (docProto && !docProto.__cxHelperVisibilityPatched) { Object.defineProperty(docProto, 'hidden', { get: () => false, configurable: true }); Object.defineProperty(docProto, 'visibilityState', { get: () => 'visible', configurable: true }); docProto.__cxHelperVisibilityPatched = true; } } catch {} try { Object.defineProperty(doc, 'hidden', { get: () => false, configurable: true }); } catch {} try { Object.defineProperty(doc, 'visibilityState', { get: () => 'visible', configurable: true }); } catch {} try { doc.hasFocus = () => true; } catch {} try { doc.onvisibilitychange = null; } catch {} try { Object.defineProperty(doc, 'onvisibilitychange', { get: () => null, set: () => {}, configurable: true }); } catch {} try { win.onblur = null; win.onpagehide = null; } catch {} }; const patchEventTarget = (win) => { try { const proto = win.EventTarget?.prototype; if (proto && !proto.__cxHelperHooked) { const originalAdd = proto.addEventListener; proto.addEventListener = function(type, listener, options) { if (VISIBILITY_EVENTS.includes(type)) return; return originalAdd.call(this, type, listener, options); }; proto.__cxHelperHooked = true; } } catch {} }; const hookBrowser = () => { try { patchVisibilityProps(document, window); patchEventTarget(window); } catch (e) { console.warn('[超星自行火炮] 环境钩子注入失败:', e); } }; hookBrowser(); const applyDocumentHooks = (doc, win) => { if (!doc || !win) return; patchVisibilityProps(doc, win); patchEventTarget(win); }; // ==================== 1. 顶层窗口检测 ==================== const isTopWindow = (function() { try { return window.self === window.top; } catch (e) { return false; } })(); // ==================== 2. 单例检查,防止多次初始化 ==================== const SCRIPT_ID = 'chaoxing-helper-initialized-v3'; const getTopWindow = () => { try { return window.top; } catch (e) { return window; } }; const topWin = getTopWindow(); if (topWin[SCRIPT_ID]) { console.log('[超星自行火炮] 已初始化,跳过重复加载'); return; } if (isTopWindow) { topWin[SCRIPT_ID] = true; } // ==================== 3. GM API 封装 ==================== const _GM_getResourceText = typeof GM_getResourceText !== 'undefined' ? GM_getResourceText : (name) => ''; const _GM_getValue = typeof GM_getValue !== 'undefined' ? GM_getValue : (key, defaultVal) => defaultVal; const _GM_info = typeof GM_info !== 'undefined' ? GM_info : { script: { name: '超星自行火炮', author: 'isMobile', version: '4.0.0' } }; const _GM_setValue = typeof GM_setValue !== 'undefined' ? GM_setValue : () => {}; const _GM_xmlhttpRequest = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : () => {}; const _unsafeWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; // ==================== 4. 工具函数 ==================== const sleep = (second) => new Promise(resolve => setTimeout(resolve, second * 1000)); const getScriptInfo = () => ({ name: _GM_info.script.name || '超星自行火炮', author: _GM_info.script.author || 'isMobile', namespace: _GM_info.script.namespace || 'chaoxing-helper', version: _GM_info.script.version || '4.0.0' }); const formatDateTime = (dt) => { const pad = n => n < 10 ? "0" + n : n.toString(); return `${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`; }; const getDateTime = () => formatDateTime(new Date()); const normalizeApiUrl = (url) => { if (!url) return ''; url = url.trim(); url = url.replace(/\/+$/, ''); if (!url.startsWith('http://') && !url.startsWith('https://')) { url = 'http://' + url; } return url; }; // ==================== 5. 自定义样式 ==================== const customStyles = ` .script-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; -webkit-font-smoothing: antialiased; } /* 日志组件 */ .script-log-container { font-size: 13px; padding: 4px; } .script-log-container .log-group-header { cursor: pointer; user-select: none; } .script-log-container .log-group-count { font-size: 11px; color: #909399; } .script-log-container .log-group-toggle { margin-left: auto; font-size: 11px; color: #409eff; } .script-log-container .log-item { padding: 8px 10px; margin: 4px 0; border-radius: 6px; background: #ffffff; box-shadow: 0 1px 4px rgba(0,0,0,0.04); line-height: 1.4; word-break: break-all; display: flex; align-items: center; gap: 8px; border-left: 3px solid transparent; } .script-log-container .log-time { color: #909399; font-family: 'Menlo', 'Monaco', monospace; font-size: 10px; flex-shrink: 0; background: #f4f6f8; padding: 2px 5px; border-radius: 3px; } .script-log-container .log-message { flex: 1; font-size: 12px; } .log-item.type-success { border-left-color: #67c23a; } .log-item.type-warning { border-left-color: #e6a23c; } .log-item.type-danger { border-left-color: #f56c6c; } .log-item.type-primary { border-left-color: #409eff; } .log-item.type-info { border-left-color: #909399; } .script-log-container .empty-log { text-align: center; color: #909399; padding: 30px 0; font-size: 12px; } /* 设置组件 */ .script-setting { font-size: 13px; padding: 4px 0; } .script-setting .setting-section { background: #fff; border: 1px solid #ebeef5; border-radius: 8px; padding: 12px 14px; margin-bottom: 10px; } .script-setting .section-title { font-size: 13px; font-weight: 600; color: #303133; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #f0f2f5; display: flex; align-items: center; gap: 6px; } .script-setting .setting-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px dashed #f0f2f5; } .script-setting .setting-item:last-child { border-bottom: none; padding-bottom: 0; } .script-setting .setting-label { font-size: 12px; color: #606266; } .script-setting .setting-tip { font-size: 10px; color: #909399; margin-top: 2px; } .script-setting .el-switch { --el-switch-on-color: #764ba2; } .script-setting .el-input-number { width: 90px; } /* API管理 */ .api-manager { padding: 4px 0; } .api-manager .api-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; margin: 4px 0; background: #f8f9fa; border-radius: 6px; border: 1px solid #ebeef5; cursor: pointer; transition: all 0.2s; } .api-manager .api-item:hover { background: #f0f2f5; } .api-manager .api-item.active { background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%); border-color: #764ba2; } .api-manager .api-item .api-url { flex: 1; font-size: 12px; color: #606266; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .api-manager .api-item.active .api-url { color: #764ba2; font-weight: 500; } .api-manager .api-item .action-btns { display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s; } .api-manager .api-item:hover .action-btns { opacity: 1; } .api-manager .api-item .action-btn { cursor: pointer; padding: 2px; border-radius: 3px; transition: background 0.2s; } .api-manager .api-item .action-btn:hover { background: rgba(0,0,0,0.05); } .api-manager .add-api { display: flex; gap: 8px; margin-top: 10px; } .api-manager .add-api .el-input { flex: 1; } .api-manager .edit-row { display: flex; align-items: center; gap: 6px; padding: 6px 8px; margin: 4px 0; background: #fff; border-radius: 6px; border: 1px solid #764ba2; box-shadow: 0 2px 8px rgba(118, 75, 162, 0.15); } .api-manager .edit-row .el-input { flex: 1; } .api-manager .edit-row .edit-actions { display: flex; gap: 4px; } .api-manager .edit-row .edit-action-btn { width: 24px; height: 24px; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; } .api-manager .edit-row .edit-action-btn.confirm { background: #67c23a; color: #fff; } .api-manager .edit-row .edit-action-btn.confirm:hover { background: #85ce61; } .api-manager .edit-row .edit-action-btn.cancel { background: #f0f2f5; color: #909399; } .api-manager .edit-row .edit-action-btn.cancel:hover { background: #e4e7ed; color: #606266; } .api-manager .empty-tip { text-align: center; color: #909399; padding: 20px 0; font-size: 12px; } /* 题目表格 */ .script-question-table { width: 100%; } .script-question-table .el-table { font-size: 11px; border-radius: 6px; --el-table-header-bg-color: #f5f7fa; } /* 主面板 */ .main-page { z-index: 100003; position: fixed; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .main-page * { box-sizing: border-box; } /* 最小化圆环 */ .mini-circle { width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, rgba(102, 126, 234, 0.25) 0%, rgba(118, 75, 162, 0.25) 100%); box-shadow: 0 1px 4px rgba(118, 75, 162, 0.1); cursor: grab; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; opacity: 0.4; user-select: none; } .mini-circle:hover { transform: scale(1.2); opacity: 0.6; box-shadow: 0 2px 8px rgba(118, 75, 162, 0.2); } .mini-circle:active { cursor: grabbing; } .mini-circle::after { content: ''; width: 6px; height: 6px; border: 1.5px solid rgba(255,255,255,0.5); border-radius: 50%; } .mini-circle.paused { background: linear-gradient(135deg, rgba(245, 108, 108, 0.3) 0%, rgba(230, 162, 60, 0.3) 100%); } /* 展开面板 */ .main-panel { width: 340px; } .main-panel .el-card { border: none; border-radius: 12px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); overflow: hidden; background: #fff; } .main-panel .el-card__header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; padding: 12px 16px; border-bottom: none; } .main-panel .el-card__header.paused { background: linear-gradient(135deg, #f56c6c 0%, #e6a23c 100%); } .main-panel .el-card__body { padding: 12px; height: 320px; overflow: hidden; } .main-panel .card-header { display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .main-panel .card-header .title { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px; } .main-panel .card-header .header-btns { display: flex; align-items: center; gap: 8px; } .main-panel .pause-btn, .main-panel .minimize-btn { width: 24px; height: 24px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; } .main-panel .pause-btn:hover, .main-panel .minimize-btn:hover { background: rgba(255,255,255,0.3); } .main-panel .minimize-btn::after { content: ''; width: 10px; height: 2px; background: #fff; border-radius: 1px; } .main-panel .pause-btn .pause-icon { width: 8px; height: 10px; display: flex; justify-content: space-between; } .main-panel .pause-btn .pause-icon::before, .main-panel .pause-btn .pause-icon::after { content: ''; width: 3px; height: 100%; background: #fff; border-radius: 1px; } .main-panel .pause-btn.playing .pause-icon { width: 0; height: 0; border-style: solid; border-width: 5px 0 5px 8px; border-color: transparent transparent transparent #fff; } .main-panel .pause-btn.playing .pause-icon::before, .main-panel .pause-btn.playing .pause-icon::after { display: none; } /* 标签页 */ .main-panel .el-tabs { height: 100%; display: flex; flex-direction: column; } .main-panel .el-tabs__header { margin-bottom: 8px; flex-shrink: 0; } .main-panel .el-tabs__content { flex: 1; overflow: hidden; } .main-panel .el-tab-pane { height: 100%; } .main-panel .el-tabs__nav-wrap::after { height: 1px; background: #ebeef5; } .main-panel .el-tabs__active-bar { background: linear-gradient(90deg, #667eea, #764ba2); height: 2px; border-radius: 2px; } .main-panel .el-tabs__item { font-size: 13px; color: #606266; padding: 0 16px; height: 36px; line-height: 36px; } .main-panel .el-tabs__item.is-active { color: #764ba2; font-weight: 600; } /* 滚动条 */ .main-panel .el-scrollbar__bar.is-vertical { width: 4px; } .main-panel .el-scrollbar__thumb { background-color: #c0c4cc; } /* 按钮 */ .main-panel .el-button--primary { --el-button-bg-color: #764ba2; --el-button-border-color: #764ba2; } /* 状态指示器 */ .status-indicator { display: flex; align-items: center; gap: 6px; padding: 8px 12px; margin-bottom: 8px; border-radius: 6px; font-size: 12px; } .status-indicator.running { background: linear-gradient(135deg, #67c23a20 0%, #85ce6120 100%); color: #67c23a; } .status-indicator.paused { background: linear-gradient(135deg, #e6a23c20 0%, #f56c6c20 100%); color: #e6a23c; } .status-indicator .status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: pulse 1.5s ease-in-out infinite; } .status-indicator.paused .status-dot { animation: none; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } `; // ==================== 6. Element Plus 图标 ==================== const createIconComponent = (name, pathD) => ({ name, render() { return vue.h('svg', { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 1024 1024", style: { width: '1em', height: '1em', fill: 'currentColor' } }, [vue.h('path', { fill: "currentColor", d: pathD })]); } }); const DeleteIcon = createIconComponent('Delete', "M160 256H96a32 32 0 0 1 0-64h256V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64h-64v672a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V256zm448-64v-64H416v64h192zM224 896h576V256H224v640zm192-128a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32zm192 0a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32z"); const CheckIcon = createIconComponent('Check', "M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768zm0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896zm-55.808-536.384l-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336L456.192 423.616z"); const EditIcon = createIconComponent('Edit', "M832 512a32 32 0 1 1 64 0v352a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h352a32 32 0 0 1 0 64H192v640h640V512z M469.952 554.24l52.8-7.552L847.104 222.4a32 32 0 1 0-45.248-45.248L477.504 501.44l-7.552 52.8zm422.4-422.4a96 96 0 0 1 0 135.808l-331.84 331.84a32 32 0 0 1-18.112 9.088L436.8 623.68a32 32 0 0 1-36.224-36.224l15.104-105.6a32 32 0 0 1 9.024-18.112l331.904-331.84a96 96 0 0 1 135.744 0z"); const CloseIcon = createIconComponent('Close', "M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"); // ==================== 7. Pinia Stores ==================== // 全局暂停状态 (响应式) const globalPauseState = vue.reactive({ isPaused: false }); let licenseLocked = false; const lockLicense = (logStore, message) => { licenseLocked = true; globalPauseState.isPaused = true; if (logStore) logStore.addLog(message || "许可验证失败,已锁定功能", "danger"); }; // 媒体处理标记 (WeakMap 防止内存泄漏) const __mediaProcessedMap = new WeakMap(); const keepAlive = (() => { let refCount = 0; let audioContext = null; let oscillator = null; let gainNode = null; let wakeLock = null; let pulseTimer = null; const startAudio = () => { try { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (!AudioCtx) return; if (!audioContext || audioContext.state === "closed") { audioContext = new AudioCtx(); } if (audioContext.state === "suspended") { audioContext.resume().catch(() => {}); } if (!oscillator) { oscillator = audioContext.createOscillator(); gainNode = audioContext.createGain(); gainNode.gain.value = 0.0001; oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); } } catch {} }; const stopAudio = () => { try { oscillator?.stop?.(); } catch {} oscillator = null; try { gainNode?.disconnect?.(); } catch {} gainNode = null; if (audioContext) { const ctx = audioContext; audioContext = null; try { ctx.close?.(); } catch {} } }; const requestWakeLock = () => { try { const wake = navigator.wakeLock; if (!wake?.request) return; wake.request("screen").then(lock => { wakeLock = lock; }).catch(() => {}); } catch {} }; const start = () => { refCount += 1; if (refCount !== 1) return; startAudio(); requestWakeLock(); if (!pulseTimer) { pulseTimer = setInterval(() => startAudio(), 30000); } }; const stop = () => { if (refCount === 0) return; refCount -= 1; if (refCount > 0) return; if (pulseTimer) { clearInterval(pulseTimer); pulseTimer = null; } if (wakeLock) { try { wakeLock.release(); } catch {} wakeLock = null; } stopAudio(); }; return { start, stop }; })(); const useConfigStore = pinia.defineStore("configStore", { state: () => { const scriptInfo = getScriptInfo(); const defaultConfig = { version: scriptInfo.version, isMinus: false, licenseKey: "", position: { x: "calc(100vw - 380px)", y: "100px" }, menuIndex: "0", platformName: "cx", platformParams: { cx: { name: "超星自行火炮", parts: [ { name: "章节设置", params: [ { name: "章节作业自动提交", value: true, type: "boolean", tip: "答题后自动提交" }, { name: "是否自动下一章节", value: true, type: "boolean", tip: "完成后自动跳转" }, { name: "跳过已完成任务点", value: true, type: "boolean", tip: "已完成的不再处理" }, { name: "只答题,不做其他", value: false, type: "boolean", tip: "跳过视频音频等" } ] }, { name: "考试设置", params: [ { name: "是否自动切换题目", value: true, type: "boolean", tip: "答完自动下一题" } ] }, { name: "视频设置", params: [ { name: "静音播放", value: true, type: "boolean", tip: "播放时静音" } ] } ] } }, otherParams: { name: "其他参数", params: [ { name: "操作间隔(秒)", value: 3, type: "number", min: 1, max: 30, tip: "每次操作的等待时间" }, { name: "重试次数", value: 3, type: "number", min: 1, max: 10, tip: "失败后重试次数" }, { name: "自动提交命中率(%)", value: 60, type: "number", min: 0, max: 100, tip: "章节测验命中率低于该值将暂存" }, { name: "许可密钥", value: "", type: "string", tip: "后台发放的许可 Key,必填" } ] }, apiList: [ "http://localhost:3000", "https://chaoxing.cloud.caqing.top" ], currentApiIndex: 1 }; let globalConfig = defaultConfig; const storedConfig = _GM_getValue("config", null); if (storedConfig) { try { const parsed = typeof storedConfig === 'string' ? JSON.parse(storedConfig) : storedConfig; globalConfig = { ...defaultConfig, ...parsed, version: scriptInfo.version, platformParams: defaultConfig.platformParams, otherParams: defaultConfig.otherParams }; // 恢复用户设置 if (parsed.platformParams?.cx?.parts) { parsed.platformParams.cx.parts.forEach((part, i) => { if (globalConfig.platformParams.cx.parts[i]) { part.params.forEach((param, j) => { if (globalConfig.platformParams.cx.parts[i].params[j]) { globalConfig.platformParams.cx.parts[i].params[j].value = param.value; } }); } }); } if (parsed.otherParams?.params) { parsed.otherParams.params.forEach((param, i) => { if (globalConfig.otherParams.params[i]) { globalConfig.otherParams.params[i].value = param.value; } }); } if (parsed.apiList?.length) { globalConfig.apiList = parsed.apiList; } if (typeof parsed.currentApiIndex === 'number') { globalConfig.currentApiIndex = parsed.currentApiIndex; } if (parsed.licenseKey) { globalConfig.licenseKey = parsed.licenseKey; } const storedLicenseParam = globalConfig.otherParams.params.find((param) => param.name === "许可密钥"); if (storedLicenseParam?.value) { globalConfig.licenseKey = storedLicenseParam.value; } } catch (e) { console.error("配置解析错误:", e); } } return globalConfig; }, getters: { currentApiUrl: (state) => { const idx = state.currentApiIndex; if (idx >= 0 && idx < state.apiList.length) { return normalizeApiUrl(state.apiList[idx]); } return state.apiList[0] ? normalizeApiUrl(state.apiList[0]) : "http://localhost:3000"; }, isPaused: () => globalPauseState.isPaused, chapterSettings: (state) => { const parts = state.platformParams.cx.parts[0].params; return { autoSubmit: parts[0].value, autoNext: parts[1].value, skipCompleted: parts[2].value, onlyQuiz: parts[3].value }; }, videoSettings: (state) => { const parts = state.platformParams.cx.parts[2].params; return { muted: parts[0].value }; } }, actions: { addApi(url) { const normalized = normalizeApiUrl(url); if (normalized && !this.apiList.includes(normalized)) { this.apiList.push(normalized); return true; } return false; }, updateApi(index, url) { const normalized = normalizeApiUrl(url); if (index >= 0 && index < this.apiList.length && normalized) { const isDuplicate = this.apiList.some((api, i) => i !== index && api === normalized); if (!isDuplicate) { this.apiList[index] = normalized; return true; } } return false; }, removeApi(index) { if (this.apiList.length > 1 && index >= 0 && index < this.apiList.length) { this.apiList.splice(index, 1); if (this.currentApiIndex >= this.apiList.length) { this.currentApiIndex = this.apiList.length - 1; } return true; } return false; }, selectApi(index) { if (index >= 0 && index < this.apiList.length) { this.currentApiIndex = index; } }, togglePause() { globalPauseState.isPaused = !globalPauseState.isPaused; return globalPauseState.isPaused; }, setPaused(value) { globalPauseState.isPaused = value; } } }); const useLogStore = pinia.defineStore("logStore", { state: () => ({ logList: [] }), actions: { addLog(message, type = 'info', meta = {}) { const entry = { message, time: getDateTime(), type, taskKey: meta.taskKey || "", taskLabel: meta.taskLabel || "" }; this.logList.push(entry); if (this.logList.length > 200) { this.logList = this.logList.slice(-100); } }, clearLogs() { this.logList = []; } } }); const useQuestionStore = pinia.defineStore("questionStore", { state: () => ({ questionList: [] }), actions: { addQuestion(question) { this.questionList.push(question); }, clearQuestion() { this.questionList = []; } } }); // ==================== 8. RxJS ==================== const { from, of, Observable } = rxjs; const { mergeMap, toArray, concatMap } = rxjs.operators || rxjs; // ==================== 9. IframeUtils ==================== class IframeUtils { static getIframes(element) { return Array.from(element.querySelectorAll("iframe")); } static getAllNestedIframes(element) { const iframes = IframeUtils.getIframes(element); if (iframes.length === 0) return of([]); return from(iframes).pipe( mergeMap(iframe => new Observable(subscriber => { try { const doc = iframe.contentDocument; const root = doc?.documentElement; if (root) { IframeUtils.getAllNestedIframes(root).subscribe({ next: nested => { subscriber.next([iframe, ...nested]); subscriber.complete(); }, error: () => { subscriber.next([iframe]); subscriber.complete(); } }); } else { subscriber.next([iframe]); subscriber.complete(); } } catch (e) { subscriber.next([iframe]); subscriber.complete(); } })), toArray(), mergeMap(arrays => of(arrays.flat())) ); } } // ==================== 10. Typr 字体解析库 ==================== const Typr = {}; Typr._bin = { readFixed: (data, o) => (data[o] << 8 | data[o + 1]) + (data[o + 2] << 8 | data[o + 3]) / (256 * 256 + 4), readF2dot14: (data, o) => Typr._bin.readShort(data, o) / 16384, readInt: (buff, p) => Typr._bin._view(buff).getInt32(p), readInt8: (buff, p) => Typr._bin._view(buff).getInt8(p), readShort: (buff, p) => Typr._bin._view(buff).getInt16(p), readUshort: (buff, p) => Typr._bin._view(buff).getUint16(p), readUshorts: (buff, p, len) => { const arr = []; for (let i = 0; i < len; i++) arr.push(Typr._bin.readUshort(buff, p + i * 2)); return arr; }, readUint: (buff, p) => Typr._bin._view(buff).getUint32(p), readUint64: (buff, p) => Typr._bin.readUint(buff, p) * (4294967295 + 1) + Typr._bin.readUint(buff, p + 4), readASCII: (buff, p, l) => { let s = ""; for (let i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]); return s; }, readBytes: (buff, p, l) => { const arr = []; for (let i = 0; i < l; i++) arr.push(buff[p + i]); return arr; }, _view: (buff) => buff._dataView || (buff._dataView = buff.buffer ? new DataView(buff.buffer, buff.byteOffset, buff.byteLength) : new DataView(new Uint8Array(buff).buffer)) }; Typr.parse = function(buff) { const bin = Typr._bin; const data = new Uint8Array(buff); const tag = bin.readASCII(data, 0, 4); if (tag === "ttcf") { let offset = 4; offset += 4; const numF = bin.readUint(data, offset); offset += 4; const fnts = []; for (let i = 0; i < numF; i++) { const foff = bin.readUint(data, offset); offset += 4; fnts.push(Typr._readFont(data, foff)); } return fnts; } return [Typr._readFont(data, 0)]; }; Typr._readFont = function(data, offset) { const bin = Typr._bin; const ooff = offset; offset += 4; const numTables = bin.readUshort(data, offset); offset += 8; const tags = ["cmap", "head", "hhea", "maxp", "hmtx", "loca", "glyf"]; const obj = { _data: data, _offset: ooff }; const tabs = {}; for (let i = 0; i < numTables; i++) { const tag = bin.readASCII(data, offset, 4); offset += 4; offset += 4; const toffset = bin.readUint(data, offset); offset += 4; const length = bin.readUint(data, offset); offset += 4; tabs[tag] = { offset: toffset, length }; } for (const t of tags) { if (tabs[t] && Typr[t]) { obj[t] = Typr[t].parse(data, tabs[t].offset, tabs[t].length, obj); } } return obj; }; Typr._tabOffset = function(data, tab, foff) { const bin = Typr._bin; const numTables = bin.readUshort(data, foff + 4); let offset = foff + 12; for (let i = 0; i < numTables; i++) { const tag = bin.readASCII(data, offset, 4); offset += 4; offset += 4; const toffset = bin.readUint(data, offset); offset += 4; offset += 4; if (tag === tab) return toffset; } return 0; }; Typr.cmap = { parse: function(data, offset, length) { data = new Uint8Array(data.buffer, offset, length); offset = 0; const bin = Typr._bin; const obj = { tables: [] }; offset += 2; const numTables = bin.readUshort(data, offset); offset += 2; const offs = []; for (let i = 0; i < numTables; i++) { const platformID = bin.readUshort(data, offset); offset += 2; const encodingID = bin.readUshort(data, offset); offset += 2; const noffset = bin.readUint(data, offset); offset += 4; const id = "p" + platformID + "e" + encodingID; let tind = offs.indexOf(noffset); if (tind === -1) { tind = obj.tables.length; offs.push(noffset); const format = bin.readUshort(data, noffset); let subt; if (format === 4) subt = Typr.cmap.parse4(data, noffset); else if (format === 12) subt = Typr.cmap.parse12(data, noffset); else subt = { format }; obj.tables.push(subt); } if (obj[id] == null) obj[id] = tind; } return obj; }, parse4: function(data, offset) { const bin = Typr._bin; const offset0 = offset; const obj = {}; obj.format = bin.readUshort(data, offset); offset += 2; const length = bin.readUshort(data, offset); offset += 2; offset += 2; const segCountX2 = bin.readUshort(data, offset); offset += 2; const segCount = segCountX2 / 2; offset += 6; obj.endCount = bin.readUshorts(data, offset, segCount); offset += segCount * 2; offset += 2; obj.startCount = bin.readUshorts(data, offset, segCount); offset += segCount * 2; obj.idDelta = []; for (let i = 0; i < segCount; i++) { obj.idDelta.push(bin.readShort(data, offset)); offset += 2; } obj.idRangeOffset = bin.readUshorts(data, offset, segCount); offset += segCount * 2; obj.glyphIdArray = []; while (offset < offset0 + length) { obj.glyphIdArray.push(bin.readUshort(data, offset)); offset += 2; } return obj; }, parse12: function(data, offset) { const bin = Typr._bin; const obj = {}; obj.format = bin.readUshort(data, offset); offset += 2; offset += 10; const nGroups = bin.readUint(data, offset); offset += 4; obj.groups = []; for (let i = 0; i < nGroups; i++) { const off = offset + i * 12; obj.groups.push([bin.readUint(data, off), bin.readUint(data, off + 4), bin.readUint(data, off + 8)]); } return obj; } }; Typr.head = { parse: function(data, offset) { const bin = Typr._bin; const obj = {}; offset += 18; obj.unitsPerEm = bin.readUshort(data, offset); offset += 2; offset += 30; obj.indexToLocFormat = bin.readShort(data, offset); return obj; } }; Typr.hhea = { parse: function(data, offset) { const bin = Typr._bin; const obj = {}; offset += 34; obj.numberOfHMetrics = bin.readUshort(data, offset); return obj; } }; Typr.maxp = { parse: function(data, offset) { const bin = Typr._bin; return { numGlyphs: bin.readUshort(data, offset + 4) }; } }; Typr.hmtx = { parse: function(data, offset, length, font) { const bin = Typr._bin; const obj = { aWidth: [], lsBearing: [] }; let aw = 0, lsb = 0; for (let i = 0; i < font.maxp.numGlyphs; i++) { if (i < font.hhea.numberOfHMetrics) { aw = bin.readUshort(data, offset); offset += 2; lsb = bin.readShort(data, offset); offset += 2; } obj.aWidth.push(aw); obj.lsBearing.push(lsb); } return obj; } }; Typr.loca = { parse: function(data, offset, length, font) { const bin = Typr._bin; const obj = []; const ver = font.head.indexToLocFormat; const len = font.maxp.numGlyphs + 1; if (ver === 0) for (let i = 0; i < len; i++) obj.push(bin.readUshort(data, offset + (i << 1)) << 1); if (ver === 1) for (let i = 0; i < len; i++) obj.push(bin.readUint(data, offset + (i << 2))); return obj; } }; Typr.glyf = { parse: function(data, offset, length, font) { const obj = []; for (let g = 0; g < font.maxp.numGlyphs; g++) obj.push(null); return obj; }, _parseGlyf: function(font, g) { const bin = Typr._bin; const data = font._data; let offset = Typr._tabOffset(data, "glyf", font._offset) + font.loca[g]; if (font.loca[g] === font.loca[g + 1]) return null; const gl = {}; gl.noc = bin.readShort(data, offset); offset += 2; gl.xMin = bin.readShort(data, offset); offset += 2; gl.yMin = bin.readShort(data, offset); offset += 2; gl.xMax = bin.readShort(data, offset); offset += 2; gl.yMax = bin.readShort(data, offset); offset += 2; if (gl.xMin >= gl.xMax || gl.yMin >= gl.yMax) return null; if (gl.noc > 0) { gl.endPts = []; for (let i = 0; i < gl.noc; i++) { gl.endPts.push(bin.readUshort(data, offset)); offset += 2; } const instructionLength = bin.readUshort(data, offset); offset += 2; if (data.length - offset < instructionLength) return null; offset += instructionLength; const crdnum = gl.endPts[gl.noc - 1] + 1; gl.flags = []; for (let i = 0; i < crdnum; i++) { const flag = data[offset]; offset++; gl.flags.push(flag); if ((flag & 8) !== 0) { const rep = data[offset]; offset++; for (let j = 0; j < rep; j++) { gl.flags.push(flag); i++; } } } gl.xs = []; for (let i = 0; i < crdnum; i++) { const i8 = (gl.flags[i] & 2) !== 0, same = (gl.flags[i] & 16) !== 0; if (i8) { gl.xs.push(same ? data[offset] : -data[offset]); offset++; } else { if (same) gl.xs.push(0); else { gl.xs.push(bin.readShort(data, offset)); offset += 2; } } } gl.ys = []; for (let i = 0; i < crdnum; i++) { const i8 = (gl.flags[i] & 4) !== 0, same = (gl.flags[i] & 32) !== 0; if (i8) { gl.ys.push(same ? data[offset] : -data[offset]); offset++; } else { if (same) gl.ys.push(0); else { gl.ys.push(bin.readShort(data, offset)); offset += 2; } } } let x = 0, y = 0; for (let i = 0; i < crdnum; i++) { x += gl.xs[i]; y += gl.ys[i]; gl.xs[i] = x; gl.ys[i] = y; } } else { gl.parts = []; let flags; do { flags = bin.readUshort(data, offset); offset += 2; const part = { m: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, p1: -1, p2: -1 }; gl.parts.push(part); part.glyphIndex = bin.readUshort(data, offset); offset += 2; let arg1, arg2; if (flags & 1) { arg1 = bin.readShort(data, offset); offset += 2; arg2 = bin.readShort(data, offset); offset += 2; } else { arg1 = bin.readInt8(data, offset); offset++; arg2 = bin.readInt8(data, offset); offset++; } if (flags & 2) { part.m.tx = arg1; part.m.ty = arg2; } else { part.p1 = arg1; part.p2 = arg2; } if (flags & 8) { part.m.a = part.m.d = Typr._bin.readF2dot14(data, offset); offset += 2; } else if (flags & 64) { part.m.a = Typr._bin.readF2dot14(data, offset); offset += 2; part.m.d = Typr._bin.readF2dot14(data, offset); offset += 2; } else if (flags & 128) { part.m.a = Typr._bin.readF2dot14(data, offset); offset += 2; part.m.b = Typr._bin.readF2dot14(data, offset); offset += 2; part.m.c = Typr._bin.readF2dot14(data, offset); offset += 2; part.m.d = Typr._bin.readF2dot14(data, offset); offset += 2; } } while (flags & 32); } return gl; } }; Typr.U = { codeToGlyph: function(font, code) { const cmap = font.cmap; let tind = -1; if (cmap.p0e4 != null) tind = cmap.p0e4; else if (cmap.p3e1 != null) tind = cmap.p3e1; else if (cmap.p1e0 != null) tind = cmap.p1e0; else if (cmap.p0e3 != null) tind = cmap.p0e3; if (tind === -1) return 0; const tab = cmap.tables[tind]; if (tab.format === 4) { let sind = -1; for (let i = 0; i < tab.endCount.length; i++) { if (code <= tab.endCount[i]) { sind = i; break; } } if (sind === -1 || tab.startCount[sind] > code) return 0; let gli = 0; if (tab.idRangeOffset[sind] !== 0) { gli = tab.glyphIdArray[code - tab.startCount[sind] + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)]; } else { gli = code + tab.idDelta[sind]; } return gli & 65535; } else if (tab.format === 12) { if (code > tab.groups[tab.groups.length - 1][1]) return 0; for (const grp of tab.groups) { if (grp[0] <= code && code <= grp[1]) return grp[2] + (code - grp[0]); } } return 0; }, glyphToPath: function(font, gid) { const path = { cmds: [], crds: [] }; if (font.glyf) Typr.U._drawGlyf(gid, font, path); return path; }, _drawGlyf: function(gid, font, path) { let gl = font.glyf[gid]; if (gl == null) gl = font.glyf[gid] = Typr.glyf._parseGlyf(font, gid); if (gl != null) { if (gl.noc > -1) Typr.U._simpleGlyph(gl, path); else Typr.U._compoGlyph(gl, font, path); } }, _simpleGlyph: function(gl, p) { for (let c = 0; c < gl.noc; c++) { const i0 = c === 0 ? 0 : gl.endPts[c - 1] + 1; const il = gl.endPts[c]; for (let i = i0; i <= il; i++) { const pr = i === i0 ? il : i - 1; const nx = i === il ? i0 : i + 1; const onCurve = gl.flags[i] & 1; const prOnCurve = gl.flags[pr] & 1; const nxOnCurve = gl.flags[nx] & 1; const x = gl.xs[i], y = gl.ys[i]; if (i === i0) { if (onCurve) { if (prOnCurve) { p.cmds.push("M"); p.crds.push(gl.xs[pr], gl.ys[pr]); } else { p.cmds.push("M"); p.crds.push(x, y); continue; } } else { if (prOnCurve) { p.cmds.push("M"); p.crds.push(gl.xs[pr], gl.ys[pr]); } else { p.cmds.push("M"); p.crds.push((gl.xs[pr] + x) / 2, (gl.ys[pr] + y) / 2); } } } if (onCurve) { if (prOnCurve) { p.cmds.push("L"); p.crds.push(x, y); } } else { if (nxOnCurve) { p.cmds.push("Q"); p.crds.push(x, y, gl.xs[nx], gl.ys[nx]); } else { p.cmds.push("Q"); p.crds.push(x, y, (x + gl.xs[nx]) / 2, (y + gl.ys[nx]) / 2); } } } p.cmds.push("Z"); } }, _compoGlyph: function(gl, font, p) { for (const prt of gl.parts) { const path = { cmds: [], crds: [] }; Typr.U._drawGlyf(prt.glyphIndex, font, path); const m = prt.m; for (let i = 0; i < path.crds.length; i += 2) { const x = path.crds[i], y = path.crds[i + 1]; p.crds.push(x * m.a + y * m.b + m.tx); p.crds.push(x * m.c + y * m.d + m.ty); } for (const cmd of path.cmds) p.cmds.push(cmd); } } }; // ==================== 11. Font 类 ==================== class Font { constructor(data) { const obj = Typr.parse(data); if (!obj?.length || typeof obj[0] !== "object") throw new Error("unable to parse font"); Object.assign(this, obj[0]); } codeToGlyph(code) { return Typr.U.codeToGlyph(this, code); } glyphToPath(gid) { return Typr.U.glyphToPath(this, gid); } } // ==================== 12. 字体解密 ==================== function decrypt(iframeDocument) { try { const styles = iframeDocument.querySelectorAll("style"); let tip = null; for (const style of styles) { if (style.textContent?.includes("font-cxsecret")) { tip = style; break; } } if (!tip) return; const fontMatch = tip.textContent.match(/base64,([\w\W]+?)'/); if (!fontMatch?.[1]) return; const fontArray = Uint8Array.from(atob(fontMatch[1]), c => c.charCodeAt(0)); const font = new Font(fontArray); const tableText = _GM_getResourceText("ttf"); if (!tableText) return; const table = JSON.parse(tableText); const match = {}; for (let i = 19968; i < 40870; i++) { const glyph = font.codeToGlyph(i); if (!glyph) continue; const path = font.glyphToPath(glyph); const hash = md5(JSON.stringify(path)).slice(24); if (table[hash]) match[i] = table[hash]; } for (const el of iframeDocument.querySelectorAll(".font-cxsecret")) { let html = el.innerHTML; for (const key in match) { html = html.replace(new RegExp(String.fromCharCode(Number(key)), "g"), String.fromCharCode(match[key])); } el.innerHTML = html; el.classList.remove("font-cxsecret"); } } catch (e) { console.warn("字体解密失败:", e); } } // ==================== 13. API调用 ==================== const getAnswer = async (question) => { const configStore = useConfigStore(); const logStore = useLogStore(); const apiUrl = configStore.currentApiUrl + '/search'; const retryCount = configStore.otherParams.params[1].value; const licenseKey = configStore.licenseKey || ""; if (licenseLocked) { logStore.addLog("许可验证未通过,功能已锁定", "danger"); return { code: 403, msg: "许可无效", data: { answer: [] } }; } if (!licenseKey) { logStore.addLog("未填写许可密钥,已取消请求", "warning"); return { code: 403, msg: "未填写许可密钥", data: { answer: [] } }; } const data = JSON.stringify({ question: question.title, options: question.optionsText, type: question.type, questionData: question.element?.outerHTML || "", workType: question.workType, licenseKey }); await sleep(configStore.otherParams.params[0].value); const tryRequest = async (attempt = 1) => { return new Promise(resolve => { _GM_xmlhttpRequest({ url: apiUrl, method: "POST", headers: { "Content-Type": "application/json" }, data, timeout: 60000, onload: response => { try { resolve(JSON.parse(response.responseText)); } catch (e) { if (attempt < retryCount) { logStore.addLog(`响应解析失败,重试 ${attempt}/${retryCount}`, "warning"); setTimeout(() => resolve(tryRequest(attempt + 1)), 1000); } else { logStore.addLog("响应解析失败", "danger"); resolve({ code: 500, data: { answer: [] }, msg: "解析失败" }); } } }, onerror: () => { if (attempt < retryCount) { logStore.addLog(`API请求失败,重试 ${attempt}/${retryCount}`, "warning"); setTimeout(() => resolve(tryRequest(attempt + 1)), 1000); } else { logStore.addLog("API请求失败", "danger"); resolve({ code: 500, data: { answer: [] }, msg: "请求失败" }); } }, ontimeout: () => { if (attempt < retryCount) { logStore.addLog(`API请求超时,重试 ${attempt}/${retryCount}`, "warning"); setTimeout(() => resolve(tryRequest(attempt + 1)), 1000); } else { logStore.addLog("API请求超时", "warning"); resolve({ code: 500, data: { answer: [] }, msg: "请求超时" }); } } }); }); }; return tryRequest(); }; // ==================== 14. 暂停检查工具 ==================== const waitIfPaused = async () => { const logStore = useLogStore(); if (globalPauseState.isPaused) { logStore.addLog("脚本已暂停,等待继续...", "warning"); while (globalPauseState.isPaused) { await sleep(0.5); } logStore.addLog("脚本继续运行", "success"); } }; // ==================== 15. 题目处理器 ==================== class BaseQuestionHandler { constructor() { this._document = document; this._window = _unsafeWindow; this.questions = []; const logStore = useLogStore(); const questionStore = useQuestionStore(); this.addLog = logStore.addLog.bind(logStore); this.addQuestion = questionStore.addQuestion.bind(questionStore); } questionType = { "单选题": "0", "A1型题": "0", "多选题": "1", "X型题": "1", "填空题": "2", "判断题": "3", "简答题": "4", "名词解释": "5", "论述题": "6", "计算题": "7" }; removeHtml(html) { if (!html) return ""; return html .replace(/<((?!img|sub|sup|br)[^>]+)>/g, "") .replace(/ /g, " ") .replace(/\s+/g, " ") .replace(//g, "\n") .replace(//g, '') .trim(); } clean(str) { if (!str) return ""; return str.replace(/^【.*?】\s*/, "").replace(/\s*(\d+(\.\d+)?分)$/, "").trim(); } } class CxQuestionHandler extends BaseQuestionHandler { constructor(type, iframe) { super(); this.type = type; if (iframe) { this._document = iframe.contentDocument; this._window = iframe.contentWindow; } } async init() { this.questions = []; this.parseHtml(); const total = this.questions.length; if (!total) { this.addLog("未解析到题目", "warning"); return { total: 0, hit: 0, hitRate: 0 }; } this.addLog(`解析到 ${total} 道题目`, "primary"); let hitCount = 0; for (const [index, question] of this.questions.entries()) { await waitIfPaused(); const answerData = await getAnswer(question); if (answerData.code === 200 && answerData.data.answer?.length) { hitCount += 1; question.answer = answerData.data.answer; this.fillQuestion(question); this.addLog(`第 ${index + 1} 题已作答`, "success"); } else { this.addLog(`第 ${index + 1} 题未找到答案`, "warning"); question.answer = [answerData.msg || "未找到答案"]; } this.addQuestion(question); } const hitRate = total ? Math.round((hitCount / total) * 100) : 0; this.addLog(`答题完成,命中率 ${hitCount}/${total} (${hitRate}%)`, hitCount ? "success" : "warning"); return { total, hit: hitCount, hitRate }; } parseHtml() { if (!this._document) return; let questionElements; if (this.type === "zj") { questionElements = this._document.querySelectorAll(".TiMu"); } else if (["zy", "ks"].includes(this.type)) { questionElements = this._document.querySelectorAll(".questionLi"); } if (questionElements?.length) { this.addQuestions(questionElements); } } extractOptions(optionElements, optionSelector) { const optionsObject = {}; const optionTexts = []; optionElements.forEach(optionElement => { const selectorEl = optionElement.querySelector(optionSelector); const optionTextContent = this.removeHtml(selectorEl?.innerHTML || ""); optionsObject[optionTextContent] = optionElement; optionTexts.push(optionTextContent); }); return [optionsObject, optionTexts]; } addQuestions(questionElements) { questionElements.forEach(questionElement => { let questionTitle = ""; let questionTypeText = ""; let optionElements; let optionsObject = {}; let optionTexts = []; if (["zy", "ks"].includes(this.type)) { const h3El = questionElement.querySelector("h3"); const titleElement = h3El?.innerHTML || ""; const colorShallowEl = questionElement.querySelector(".colorShallow"); const colorShallowElement = colorShallowEl?.outerHTML || ""; if (this.type === "zy") { questionTypeText = questionElement.getAttribute("typename") || ""; } else if (this.type === "ks") { questionTypeText = this.removeHtml(colorShallowElement).slice(1, 4) || ""; } questionTitle = this.removeHtml(titleElement.split(colorShallowElement || "@@NOSPLIT@@")[1] || titleElement); optionElements = questionElement.querySelectorAll(".answerBg"); [optionsObject, optionTexts] = this.extractOptions(optionElements, ".answer_p"); } else if (this.type === "zj") { const fontLabelEl = questionElement.querySelector(".fontLabel"); const zyTitleEl = questionElement.querySelector(".newZy_TItle"); questionTitle = this.removeHtml(fontLabelEl?.innerHTML || ""); questionTypeText = this.removeHtml(zyTitleEl?.innerHTML || ""); optionElements = questionElement.querySelectorAll('[class*="before-after"]'); [optionsObject, optionTexts] = this.extractOptions(optionElements, ".fl.after"); } const cleanType = questionTypeText.replace(/【|】/g, "").trim(); this.questions.push({ element: questionElement, type: this.questionType[cleanType] || "999", title: this.clean(questionTitle), optionsText: optionTexts, options: optionsObject, answer: [], workType: this.type, refer: this._window?.location.href || window.location.href }); }); } fillQuestion(question) { if (!this._window) return; try { if (question.type === "0" || question.type === "1") { question.answer.forEach(answer => { const cleanAnswer = this.removeHtml(answer); for (const key in question.options) { if (key === cleanAnswer) { const optionElement = question.options[key]; if (["zj", "zy"].includes(this.type)) { if (optionElement.getAttribute("aria-checked") !== "true") optionElement.click(); } else if (this.type === "ks") { if (!optionElement.querySelector(".check_answer, .check_answer_dx")) optionElement.click(); } } } }); } else if (question.type === "2") { const textareaElements = question.element.querySelectorAll("textarea"); textareaElements.forEach((textareaElement, index) => { const answerText = question.answer[index] || ""; try { const ueditor = this._window.UE?.getEditor(textareaElement.name); if (ueditor?.setContent) { ueditor.setContent(answerText); return; } } catch (e) {} textareaElement.value = answerText; }); } else if (question.type === "3") { let answer = "true"; const firstAnswer = question.answer[0] || ""; if (firstAnswer.match(/(^|,)(正确|是|对|√|T|ri|right|true)(,|$)/i)) answer = "true"; else if (firstAnswer.match(/(^|,)(错误|否|错|×|F|wr|wrong|false)(,|$)/i)) answer = "false"; const trueOrFalse = { "true": "对", "false": "错" }; for (const key in question.options) { const optEl = question.options[key]; if (["zj", "zy"].includes(this.type)) { const ariaLabel = optEl.getAttribute("aria-label") || ""; if (ariaLabel.includes(`${trueOrFalse[answer]}选择`)) { if (optEl.getAttribute("aria-checked") !== "true") optEl.click(); } } else if (this.type === "ks") { const optionElement = optEl.querySelector(`span[data='${answer}']`); if (optionElement && !optionElement.querySelector(".check_answer")) optionElement.click(); } } } else if (question.type === "4" || question.type === "6") { const textareaElement = question.element.querySelector("textarea"); if (!textareaElement) return; const answerText = question.answer[0] || ""; try { const ueditor = this._window.UE?.getEditor(textareaElement.name); if (ueditor?.setContent) { ueditor.setContent(answerText); return; } } catch (e) {} textareaElement.value = answerText; } } catch (e) { console.warn("填写答案出错:", e); } } } // ==================== 16. 章节学习逻辑(简化重写)==================== const useCxChapterLogic = () => { const logStore = useLogStore(); const configStore = useConfigStore(); if (licenseLocked) { logStore.addLog("许可验证未通过,功能已锁定", "danger"); return; } let currentTaskId = 0; const RECOVERY_CONFIG = { inactivityRefreshMs: 6 * 60 * 1000, taskStuckRefreshMs: 3 * 60 * 1000, refreshCooldownMs: 90 * 1000 }; const SMART_CHECK_CONFIG = { initialDelayMs: 60000, intervalMs: 25000, rescanCooldownMs: 8000, pendingMaxMs: 2 * 60 * 1000, staleTaskTtlMs: 10 * 60 * 1000, missingTaskRefreshMs: 2 * 60 * 1000 }; let smartCheckSessionId = 0; let smartCheckDelayTimer = null; let smartCheckTimer = null; let monitorTimer = null; let lastActivityAt = Date.now(); let lastProgressAt = Date.now(); let lastRefreshAt = 0; let activeTaskKey = ""; let activeTaskLabel = ""; let lastSmartScanAt = 0; let lastSmartActionAt = 0; let smartCheckRunning = false; let smartKeepAlive = false; let chapterComplete = false; const taskRegistry = new Map(); let isScanning = false; let pendingScanRoot = null; const touchActivity = () => { lastActivityAt = Date.now(); }; const touchProgress = () => { const now = Date.now(); lastProgressAt = now; lastActivityAt = now; }; const updateChapterCompletion = (total, pending) => { if (total > 0) { chapterComplete = pending === 0; return; } if (pending > 0) chapterComplete = false; }; const resetChapterCompletion = () => { chapterComplete = false; }; const touchTaskState = (meta, patch = {}) => { if (!meta?.taskKey) return; const now = Date.now(); const current = taskRegistry.get(meta.taskKey) || { firstSeenAt: now, lastSeenAt: now, lastProgressAt: now, finished: false, saved: false }; current.lastSeenAt = now; if (patch.progress) current.lastProgressAt = now; if (patch.finished === true) current.finished = true; if (patch.saved === true) current.saved = true; if (patch.saved === false) current.saved = false; if (patch.type) current.type = patch.type; taskRegistry.set(meta.taskKey, current); }; const markTaskProgress = (meta) => { touchProgress(); touchTaskState(meta, { progress: true, type: meta?.taskType }); }; const updateSmartKeepAlive = (shouldKeep) => { if (shouldKeep && !smartKeepAlive) { keepAlive.start(); smartKeepAlive = true; return; } if (!shouldKeep && smartKeepAlive) { keepAlive.stop(); smartKeepAlive = false; } }; const setActiveTask = (meta) => { activeTaskKey = meta?.taskKey || ""; activeTaskLabel = meta?.taskLabel || ""; markTaskProgress(meta); }; const clearActiveTask = (meta) => { if (!meta?.taskKey || meta.taskKey === activeTaskKey) { activeTaskKey = ""; activeTaskLabel = ""; touchProgress(); } }; const autoRecover = (reason) => { if (globalPauseState.isPaused) return; if (chapterComplete) return; const now = Date.now(); if (now - lastRefreshAt < RECOVERY_CONFIG.refreshCooldownMs) return; if (configStore.chapterSettings.onlyQuiz && !hasPendingWork()) return; if (!hasUnfinishedJobs() && !activeTaskKey) return; lastRefreshAt = now; logStore.addLog(`自动刷新:${reason}`, "warning"); setTimeout(() => window.location.reload(), 1200); }; const markFailure = (reason) => { touchActivity(); runSmartCheck("failure"); autoRecover(reason); }; const scheduleSmartCheck = () => { smartCheckSessionId += 1; const sessionId = smartCheckSessionId; if (smartCheckDelayTimer) clearTimeout(smartCheckDelayTimer); smartCheckDelayTimer = setTimeout(() => { if (sessionId !== smartCheckSessionId) return; if (globalPauseState.isPaused) return; runSmartCheck("initial"); startSmartInspector(); }, SMART_CHECK_CONFIG.initialDelayMs); }; const startRecoveryMonitor = () => { if (monitorTimer) clearInterval(monitorTimer); monitorTimer = setInterval(() => { if (globalPauseState.isPaused) return; if (chapterComplete) return; if (configStore.chapterSettings.onlyQuiz && !hasPendingWork()) return; const now = Date.now(); if (activeTaskKey && now - lastProgressAt > RECOVERY_CONFIG.taskStuckRefreshMs) { runSmartCheck("stuck"); autoRecover(`任务点长时间无进展${activeTaskLabel ? ":" + activeTaskLabel : ""}`); return; } if (hasUnfinishedJobs() && now - lastActivityAt > RECOVERY_CONFIG.inactivityRefreshMs) { runSmartCheck("idle"); autoRecover("长时间无动作"); } }, 15000); }; const init = () => { const currentUrl = window.location.href; if (!currentUrl.includes("&mooc2=1")) { window.location.href = currentUrl + "&mooc2=1"; } logStore.addLog("检测到用户进入到章节学习页面", "primary"); logStore.addLog("正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)", "warning"); touchActivity(); scheduleSmartCheck(); startRecoveryMonitor(); }; const processIframeTask = () => { const documentElement = document.documentElement; const mainIframe = getMainIframe(); if (!mainIframe) { console.warn("No iframe found."); return; } touchActivity(); const getWatchRoot = () => safeDoc(mainIframe)?.documentElement || documentElement; watchIframe(getWatchRoot()); if (!mainIframe.__cxHelperWatchBound) { mainIframe.addEventListener("load", () => watchIframe(getWatchRoot())); mainIframe.__cxHelperWatchBound = true; } }; const setupInterceptor = () => { let currentUrl = window.location.href; let currentChapterId = getChapterId(); let currentIframeSrc = getMainIframe()?.src || ""; setInterval(() => { const nextUrl = window.location.href; const nextChapterId = getChapterId(); const nextIframeSrc = getMainIframe()?.src || ""; if (currentUrl !== nextUrl || currentChapterId !== nextChapterId || currentIframeSrc !== nextIframeSrc) { currentUrl = nextUrl; currentChapterId = nextChapterId; currentIframeSrc = nextIframeSrc; touchActivity(); taskRegistry.clear(); activeTaskKey = ""; activeTaskLabel = ""; isScanning = false; pendingScanRoot = null; resetChapterCompletion(); scheduleSmartCheck(); processIframeTask(); } }, 2000); }; const watchIframe = (scanRoot) => { if (!scanRoot) return; if (isScanning) { pendingScanRoot = scanRoot; return; } isScanning = true; touchActivity(); const thisTaskId = ++currentTaskId; IframeUtils.getAllNestedIframes(scanRoot).subscribe({ next: (allIframes) => { const taskIframes = allIframes.filter((iframe) => { const src = iframe?.src || ""; if (!src || src.includes("javascript:")) return false; if (src.includes("api/work")) return true; const parent = iframe.parentElement; if (!parent) return false; const hasJobIcon = parent.querySelector(".ans-job-icon") !== null; if (!hasJobIcon) return false; const isMedia = src.includes("video") || src.includes("audio"); return isMedia || isDocTask(src) || isBookTask(src); }); taskIframes.forEach((iframe, index) => { iframe.__cxTaskOrder = index + 1; iframe.__cxTaskTotal = taskIframes.length; }); from(allIframes).pipe(concatMap((iframe) => processIframe(iframe, thisTaskId))).subscribe({ complete: async () => { isScanning = false; if (pendingScanRoot) { const nextRoot = pendingScanRoot; pendingScanRoot = null; setTimeout(() => watchIframe(nextRoot), 500); return; } if (thisTaskId === currentTaskId) { const stats = await getTaskStats(); if (stats.pendingCount > 0) { logStore.addLog(`检测到仍有未完成任务点(${stats.pendingCount}/${stats.total}),暂不跳转`, "warning"); return; } logStore.addLog("本页任务点已全部完成,正前往下一章节", "success"); if (configStore.platformParams.cx.parts[0].params[1].value) { await sleep(3); const jumped = await tryJumpNextChapter(); if (!jumped) { logStore.addLog("未找到下一章节入口,稍后将继续尝试", "warning"); runSmartCheck("next_missing"); } else { setTimeout(() => handleJobFinishTip(), 1500); } } else { logStore.addLog("已经关闭自动下一章节,在设置里可更改", "danger"); } } }, error: (e) => { console.warn("任务点串行处理异常:", e); isScanning = false; } }); }, error: (e) => { console.warn("任务点扫描异常:", e); isScanning = false; } }); }; // 安全获取 iframe 的 contentDocument const safeDoc = (iframe) => { try { return iframe.contentDocument; } catch { return null; } }; const waitIframeLoad = (iframe) => { return new Promise((resolve) => { let waited = 0; const timer = setInterval(() => { const doc = safeDoc(iframe); if (doc && doc.readyState === "complete") { clearInterval(timer); resolve(); return; } waited += 500; if (waited >= 15000) { clearInterval(timer); resolve(); } }, 500); }); }; const docTypes = ["ppt", "doc", "pptx", "docx", "pdf"]; const isDocTask = (src) => docTypes.some((type) => src.includes(`modules/${type}`)); const isBookTask = (src) => src.includes("modules/innerbook"); let taskSeq = 0; const cleanText = (text) => { if (!text) return ""; return String(text).replace(/\s+/g, " ").replace(/\u00a0/g, " ").trim(); }; const includesAny = (text, items) => items.some((item) => text.includes(item)); const shortText = (text, max = 24) => { if (!text) return ""; const clean = cleanText(text); if (clean.length <= max) return clean; return clean.slice(0, max) + "..."; }; const getRootDoc = () => { try { return window.top.document; } catch { return document; } }; const getMainIframe = () => { const rootDoc = getRootDoc(); return rootDoc.querySelector("#iframe") || rootDoc.querySelector("iframe"); }; const getPageLabel = () => { const rootDoc = getRootDoc(); const selectors = [ "#coursetree .currents .posCatalog_name", "#coursetree .currents .catalog_name", "#coursetree .currents", "#coursetree .current", ".courseChapter .currents", ".courseChapter .current", ".catalog .currents", ".catalog .current", ".prev_next .cur", ".prev_next .cur .catalog_name", ".breadcrumb .active", ".catalog_name", ".chapterName", ".courseName", ".course-title", ".article-title", ".title", "h1", "h2" ]; for (const sel of selectors) { const el = rootDoc.querySelector(sel); const text = cleanText(el?.textContent || ""); if (text) return shortText(text, 32); } return shortText(rootDoc.title || document.title || "", 32); }; const getChapterInfo = () => { const rootDoc = getRootDoc(); const activeNode = rootDoc.querySelector(".posCatalog_select.posCatalog_active .posCatalog_name") || rootDoc.querySelector(".posCatalog_select.posCatalog_active"); const code = cleanText(activeNode?.querySelector(".posCatalog_sbar")?.textContent || ""); let title = cleanText(activeNode?.getAttribute("title") || activeNode?.textContent || ""); if (code && title.includes(code)) { title = cleanText(title.replace(code, "")); } if (!title) { title = getPageLabel(); } const parts = code ? code.split(".").filter(Boolean) : []; let chapterLabel = ""; if (parts[0]) chapterLabel += `第${parts[0]}章`; if (parts[1]) chapterLabel += `第${parts[1]}节`; return { code, title, chapterLabel }; }; const getChapterId = () => { const rootDoc = getRootDoc(); return rootDoc.querySelector("#chapterIdid")?.value || rootDoc.querySelector("#curChapterId")?.value || ""; }; const isTaskFinished = (iframe) => { const parent = iframe?.parentElement; if (!parent) return false; return parent.classList.contains("ans-job-finished"); }; const hasUnfinishedJobs = () => { const mainIframe = getMainIframe(); const innerDoc = safeDoc(mainIframe); if (!innerDoc) return false; const jobIcons = Array.from(innerDoc.querySelectorAll(".ans-job-icon")); if (!jobIcons.length) return false; return jobIcons.some((icon) => !icon.parentElement?.classList.contains("ans-job-finished")); }; const scrollToUnfinishedJob = () => { try { if (typeof _unsafeWindow.scroll2Job === "function") { _unsafeWindow.scroll2Job(); return true; } } catch {} const mainIframe = getMainIframe(); const innerDoc = safeDoc(mainIframe); if (!innerDoc) return false; const jobIcons = Array.from(innerDoc.querySelectorAll(".ans-job-icon")); const target = jobIcons.find((icon) => !icon.parentElement?.classList.contains("ans-job-finished")); if (!target) return false; const node = target.parentElement || target; try { node.scrollIntoView({ behavior: "smooth", block: "center" }); } catch { node.scrollIntoView(); } return true; }; let lastJobTipAt = 0; const handleJobFinishTip = () => { const now = Date.now(); if (now - lastJobTipAt < 3000) return false; const rootDoc = getRootDoc(); const tips = [ rootDoc.querySelector(".jobFinishTip"), rootDoc.querySelector("#jobFinishTip") ].filter(Boolean); if (!tips.length) return false; const isVisible = (el) => { try { if (el.style?.display === "none") return false; const style = getComputedStyle(el); return style.display !== "none" && style.visibility !== "hidden"; } catch { return true; } }; let handled = false; for (const tip of tips) { if (!isVisible(tip)) continue; const goLearn = tip.querySelector(".btnBlue.btn_92_cancel, .graybtn02.nextbutton, .popMoveDele"); if (goLearn?.click) { goLearn.click(); handled = true; break; } const closeBtn = tip.querySelector(".popClose"); if (closeBtn?.click) { closeBtn.click(); handled = true; break; } } if (handled) { lastJobTipAt = now; try { _unsafeWindow.WAY?.box?.hide?.(); } catch {} logStore.addLog("检测到未完成提示,已返回任务点", "warning"); } return handled; }; const getAllIframes = (scanRoot) => new Promise((resolve) => { if (!scanRoot) return resolve([]); IframeUtils.getAllNestedIframes(scanRoot).subscribe({ next: (allIframes) => resolve(allIframes || []), error: () => resolve([]) }); }); const getTaskTypeFromSrc = (src) => { if (src.includes("api/work")) return "work"; if (src.includes("video")) return "video"; if (src.includes("audio")) return "audio"; if (isDocTask(src)) return "doc"; if (isBookTask(src)) return "book"; return "task"; }; const getTaskIframes = (allIframes) => allIframes.filter((iframe) => { const src = iframe?.src || ""; if (!src || src.includes("javascript:")) return false; if (src.includes("api/work")) return true; const parent = iframe.parentElement; if (!parent) return false; const hasJobIcon = parent.querySelector(".ans-job-icon") !== null; if (!hasJobIcon) return false; return src.includes("video") || src.includes("audio") || isDocTask(src) || isBookTask(src); }); const getDocTextHint = (doc) => { const body = doc?.body; if (!body) return ""; const text = typeof body.innerText === "string" ? body.innerText : (body.textContent || ""); return text.length > 8000 ? text.slice(0, 8000) : text; }; const getMediaDuration = (mediaEl) => { const duration = Number.isFinite(mediaEl?.duration) ? mediaEl.duration : null; if (!duration || duration <= 0) return null; return duration; }; const isMediaFinished = (mediaEl) => { if (!mediaEl) return false; if (mediaEl.ended) return true; const duration = getMediaDuration(mediaEl); if (!duration) return false; const threshold = Math.max(duration - 0.5, duration * 0.98); return mediaEl.currentTime >= threshold; }; const isWorkFinishedByDoc = (doc) => { if (!doc) return false; const hint = cleanText(getDocTextHint(doc)); if (!hint) return false; const doneAttrSelectors = [ "[title*='已完成']", "[title*='已提交']", "[title*='已交卷']", "[title*='已批阅']", "[title*='待批阅']", "[alt*='已完成']", "[alt*='已提交']", "[alt*='已交卷']", "[alt*='已批阅']", "[alt*='待批阅']" ]; if (doneAttrSelectors.some((selector) => doc.querySelector(selector))) return true; const submitBtn = doc.querySelector( "#btnBlueSubmit, .btnBlueSubmit, .btnBlue, .bluebtn, .btnSubmit, button[type='submit'], input[type='submit']" ); const submitText = cleanText(submitBtn?.textContent || submitBtn?.value || ""); const submitDisabled = !!submitBtn && ( submitBtn.disabled || submitBtn.getAttribute("disabled") !== null || submitBtn.getAttribute("aria-disabled") === "true" || submitBtn.classList?.contains("disabled") || submitBtn.classList?.contains("btn-disabled") ); const redoHints = ["重新作答", "再做", "重做", "重新答题", "继续作答", "继续答题", "再次作答", "再次答题", "重新做题", "查看解析", "查看答案", "查看成绩", "查看结果", "查看报告"]; if (submitText && includesAny(submitText, redoHints)) return true; const actionEls = Array.from(doc.querySelectorAll("button, a, input[type='button'], input[type='submit']")); const hasRedoAction = actionEls.some((el) => { const text = cleanText(el?.textContent || el?.value || ""); return text && includesAny(text, redoHints); }); if (hasRedoAction) return true; const resultSelectors = [ ".score", ".score-wrap", ".analysis", ".work-result", ".ans-job-finished", ".workResult", ".exam-result", ".paper-result", ".resultScore", ".scoreView", ".scoreCon", ".analysisResult", ".analysis_wrap", ".check_answer", ".check_answer_dx", ".answer-analysis", ".analysis_cont", ".analysisCont", ".right_answer", ".correctAnswer", ".answerKey" ]; const hasResultPanel = resultSelectors.some((selector) => doc.querySelector(selector)); const hasScoreText = /得分\s*\d+|成绩\s*[::]?\s*\d+|分数\s*[::]?\s*\d+/.test(hint); const questions = doc.querySelectorAll(".TiMu, .questionLi"); const inputs = doc.querySelectorAll("input, textarea, select"); const hasInputs = inputs.length > 0; const hasEditableInput = hasInputs && Array.from(inputs).some((el) => { const disabled = el.disabled || el.getAttribute("disabled") !== null; const readonly = el.readOnly || el.getAttribute("readonly") !== null; return !(disabled || readonly); }); if (questions.length > 0 && hasInputs && !hasEditableInput) return true; if ((hasResultPanel || hasScoreText) && (!submitBtn || submitDisabled || !hasEditableInput)) return true; const doneHints = [ "作业已完成", "本次作业已完成", "已提交", "已交卷", "已批阅", "待批阅", "已评分", "成绩", "得分", "提交时间", "完成率100", "答题完成", "提交成功", "正确答案", "查看解析" ]; const hasDoneHint = includesAny(hint, doneHints); if (hasDoneHint) { const notDoneHints = ["未完成", "未提交", "未作答", "未答题", "未交", "未做", "未交卷", "未开始"]; if (!includesAny(hint, notDoneHints) || hasResultPanel || hasScoreText || submitDisabled) return true; } return false; }; const getTaskDetail = (iframe) => { const src = iframe?.src || ""; const doc = safeDoc(iframe); const type = getTaskTypeFromSrc(src); const meta = buildTaskMeta(iframe); const state = taskRegistry.get(meta.taskKey) || {}; const saved = !!state.saved; const finishedByDom = isTaskFinished(iframe); let finishedByContent = false; let mediaTime = null; if (doc) { if (type === "work") { finishedByContent = isWorkFinishedByDoc(doc); } else if (type === "video" || type === "audio") { const mediaEl = doc.querySelector(type === "video" ? "video" : "audio"); if (mediaEl) { mediaTime = Number.isFinite(mediaEl.currentTime) ? mediaEl.currentTime : null; finishedByContent = isMediaFinished(mediaEl); } } else if (type === "doc" || type === "book") { const hint = getDocTextHint(doc); finishedByContent = /已完成/.test(hint); } } return { iframe, meta, type, src, finished: finishedByDom || finishedByContent || !!state.finished || saved, saved, mediaTime }; }; const updateTaskRegistry = (details) => { const now = Date.now(); const seen = new Set(); details.forEach((detail) => { const key = detail.meta?.taskKey; if (!key) return; seen.add(key); const current = taskRegistry.get(key) || { firstSeenAt: now, lastSeenAt: now, lastProgressAt: now, lastMediaTime: null, finished: false, saved: false }; current.lastSeenAt = now; if (detail.mediaTime != null) { if (current.lastMediaTime == null || detail.mediaTime > current.lastMediaTime + 0.1) { current.lastMediaTime = detail.mediaTime; current.lastProgressAt = now; } } if (detail.saved) current.saved = true; if (detail.finished && !current.finished) { current.completedAt = now; } current.finished = current.finished || current.saved || detail.finished; current.type = detail.type; taskRegistry.set(key, current); }); for (const [key, state] of taskRegistry) { if (!seen.has(key) && now - state.lastSeenAt > SMART_CHECK_CONFIG.staleTaskTtlMs) { taskRegistry.delete(key); } } }; const hasPendingWork = () => { for (const state of taskRegistry.values()) { if (state.type === "work" && !state.finished) return true; } return false; }; const getOldestPendingMs = (pending) => { const now = Date.now(); let oldest = 0; pending.forEach((detail) => { const state = taskRegistry.get(detail.meta?.taskKey || ""); const baseTime = state?.lastProgressAt || state?.lastSeenAt || now; const age = now - baseTime; if (age > oldest) oldest = age; }); return oldest; }; const getTaskStats = async () => { const mainIframe = getMainIframe(); const scanRoot = safeDoc(mainIframe)?.documentElement || document.documentElement; const allIframes = await getAllIframes(scanRoot); const taskIframes = getTaskIframes(allIframes); const details = taskIframes.map(getTaskDetail); updateTaskRegistry(details); const focusDetails = configStore.chapterSettings.onlyQuiz ? details.filter(detail => detail.type === "work") : details; const pending = focusDetails.filter(detail => !detail.finished); const pendingCount = hasUnfinishedJobs() ? Math.max(pending.length, 1) : pending.length; updateChapterCompletion(focusDetails.length, pendingCount); return { total: focusDetails.length, pendingCount }; }; const findNextChapterTarget = () => { const rootDoc = getRootDoc(); const current = rootDoc.querySelector(".posCatalog_select.posCatalog_active") || rootDoc.querySelector("#coursetree .currents") || rootDoc.querySelector("#coursetree .current") || rootDoc.querySelector(".courseChapter .currents") || rootDoc.querySelector(".courseChapter .current"); if (!current) return null; let node = current.nextElementSibling; while (node) { if (node.nodeType !== 1) { node = node.nextElementSibling; continue; } const disabled = node.classList.contains("disabled") || node.classList.contains("posCatalog_disabled") || node.classList.contains("posCatalog_locked"); const target = node.querySelector(".posCatalog_name") || node.querySelector(".catalog_name") || node.querySelector("a") || node; if (!disabled && target) { return target; } node = node.nextElementSibling; } return null; }; const tryJumpNextChapter = async () => { const rootDoc = getRootDoc(); const nextBtn = rootDoc.querySelector("#prevNextFocusNext"); if (nextBtn && nextBtn.style.display !== "none") { nextBtn.click(); return true; } const jumpBtn = rootDoc.querySelector(".jb_btn.jb_btn_92.fr.fs14.nextChapter"); if (jumpBtn) { jumpBtn.click(); return true; } const target = findNextChapterTarget(); if (target?.click) { target.click(); return true; } return false; }; const runSmartCheck = async (source = "interval") => { if (smartCheckRunning || globalPauseState.isPaused) return; if (source === "interval" && Date.now() - lastSmartScanAt < SMART_CHECK_CONFIG.intervalMs * 0.6) return; smartCheckRunning = true; lastSmartScanAt = Date.now(); try { const handledTip = handleJobFinishTip(); if (handledTip && !isScanning && !activeTaskKey) { lastSmartActionAt = Date.now(); processIframeTask(); } const mainIframe = getMainIframe(); const scanRoot = safeDoc(mainIframe)?.documentElement || document.documentElement; const allIframes = await getAllIframes(scanRoot); const taskIframes = getTaskIframes(allIframes); const details = taskIframes.map(getTaskDetail); updateTaskRegistry(details); const focusDetails = configStore.chapterSettings.onlyQuiz ? details.filter(detail => detail.type === "work") : details; const total = focusDetails.length; const pending = focusDetails.filter(detail => !detail.finished); const pendingCount = hasUnfinishedJobs() ? Math.max(pending.length, 1) : pending.length; updateChapterCompletion(total, pendingCount); if (total === 0) { updateSmartKeepAlive(false); const shouldCheckMissing = !configStore.chapterSettings.onlyQuiz || hasPendingWork(); if (shouldCheckMissing && hasUnfinishedJobs()) { if (!isScanning && Date.now() - lastSmartActionAt > SMART_CHECK_CONFIG.rescanCooldownMs) { lastSmartActionAt = Date.now(); if (scrollToUnfinishedJob()) { logStore.addLog("智能检查:已定位未完成任务点", "warning"); } logStore.addLog("智能检查:任务点未加载,尝试重新扫描", "warning"); processIframeTask(); } if (Date.now() - lastActivityAt > SMART_CHECK_CONFIG.missingTaskRefreshMs) { autoRecover("智能检查:任务点未加载"); } } return; } updateSmartKeepAlive(pendingCount > 0); if (pendingCount === 0) return; const activeExists = activeTaskKey && focusDetails.some(detail => detail.meta?.taskKey === activeTaskKey); if (activeTaskKey && !activeExists) { clearActiveTask(); } if (!isScanning && !activeTaskKey && pendingCount > 0) { const now = Date.now(); if (now - lastSmartActionAt > SMART_CHECK_CONFIG.rescanCooldownMs) { lastSmartActionAt = now; if (scrollToUnfinishedJob()) { logStore.addLog("智能检查:已定位未完成任务点", "warning"); } logStore.addLog("智能检查:发现未完成任务点,重新排查", "warning"); processIframeTask(); } } const oldestPendingMs = getOldestPendingMs(pending); if (oldestPendingMs > SMART_CHECK_CONFIG.pendingMaxMs) { autoRecover("智能检查:任务点长时间未完成"); } } finally { smartCheckRunning = false; } }; const startSmartInspector = () => { if (smartCheckTimer) return; smartCheckTimer = setInterval(() => runSmartCheck("interval"), SMART_CHECK_CONFIG.intervalMs); }; const extractChapterSection = (text) => { const clean = cleanText(text); if (!clean) return ""; const chapterMatch = clean.match(/第\s*([0-9一二三四五六七八九十百]+)\s*章/); const sectionMatch = clean.match(/第\s*([0-9一二三四五六七八九十百]+)\s*节/); let chapter = chapterMatch?.[1] || ""; let section = sectionMatch?.[1] || ""; if (!chapter || !section) { const numberMatch = clean.match(/(\d+(?:\.\d+)+)/); if (numberMatch) { const parts = numberMatch[1].split(".").filter(Boolean); if (!chapter && parts[0]) chapter = parts[0]; if (!section && parts[1]) section = parts[1]; } } if (!chapter && !section) return ""; return `${chapter ? `第${chapter}章` : ""}${section ? `第${section}节` : ""}`; }; const getIframeJobInfo = (iframe) => { let jobId = iframe?.getAttribute?.("jobid") || ""; let objectId = iframe?.getAttribute?.("objectid") || ""; let mid = iframe?.getAttribute?.("mid") || ""; let title = iframe?.getAttribute?.("title") || ""; const dataAttr = iframe?.getAttribute?.("data") || ""; if (dataAttr) { try { const data = JSON.parse(dataAttr); jobId = jobId || data.jobid || data._jobid || ""; objectId = objectId || data.objectid || data.objectId || ""; mid = mid || data.mid || ""; title = title || data.name || data.title || ""; } catch {} } return { jobId, objectId, mid, title }; }; const buildTaskMeta = (iframe) => { const src = iframe?.src || ""; const taskType = getTaskTypeFromSrc(src); let typeName = "任务点"; if (taskType === "work") typeName = "作业"; else if (taskType === "video") typeName = "视频"; else if (taskType === "audio") typeName = "音频"; else if (taskType === "doc") typeName = "文档"; else if (taskType === "book") typeName = "电子书"; const { jobId, objectId, mid } = getIframeJobInfo(iframe); let id = ""; try { const url = new URL(src, window.location.href); id = url.searchParams.get("jobid") || url.searchParams.get("jobId") || url.searchParams.get("objectid") || url.searchParams.get("objectId") || url.searchParams.get("mid") || url.searchParams.get("knowledgeId") || ""; } catch {} if (!id) { id = jobId || objectId || mid || ""; } if (!iframe.__cxTaskKey) { const chapterId = getChapterId(); const keyParts = [typeName, chapterId, jobId || objectId || mid || ""].filter(Boolean); if (keyParts.length >= 2) { iframe.__cxTaskKey = keyParts.join("-"); } else if (id) { iframe.__cxTaskKey = `${typeName}-${id}`; } else if (iframe?.__cxTaskOrder) { iframe.__cxTaskKey = `${typeName}-idx-${iframe.__cxTaskOrder}`; } else if (src) { iframe.__cxTaskKey = `${typeName}-${src}`; } else { iframe.__cxTaskKey = `task-${++taskSeq}`; } } const labelParts = []; const order = Number(iframe?.__cxTaskOrder || 0); const chapterInfo = getChapterInfo(); const chapterSection = chapterInfo.chapterLabel || extractChapterSection(chapterInfo.title || ""); if (chapterSection) labelParts.push(chapterSection); if (order) { labelParts.push(`第${order}个任务点`); } else { labelParts.push("任务点"); } return { taskKey: iframe.__cxTaskKey, taskLabel: labelParts.join(" ").trim() || "任务点", taskType }; }; const createTaskLogger = (iframe) => { const meta = buildTaskMeta(iframe); const log = (message, type = "info") => { touchActivity(); logStore.addLog(message, type, meta); }; return { log, meta }; }; // 核心:处理视频/音频(暴力轮询,替代复杂的 VideoGuard) const processMedia = (mediaType, doc, win, taskId, addLog, taskMeta) => { return new Promise((resolve) => { const log = addLog || ((message, type = "info") => logStore.addLog(message, type)); const typeName = mediaType === 'video' ? '视频' : '音频'; const checkMedia = () => { try { const media = doc.querySelector(mediaType); if (!media) return null; return media; } catch { return null; } }; const tryAutoPlay = async (mediaEl) => { if (!mediaEl) return; if (configStore.videoSettings.muted) { mediaEl.muted = true; mediaEl.volume = 0; } mediaEl.autoplay = true; try { await mediaEl.play(); } catch (e) { try { mediaEl.muted = true; mediaEl.volume = 0; await mediaEl.play(); } catch {} } }; let media = checkMedia(); if (!media) { // 等待媒体元素出现 let waitCount = 0; const waitTimer = setInterval(() => { media = checkMedia(); waitCount++; if (media || waitCount > 30) { // 最多等30秒 clearInterval(waitTimer); if (!media) { log(`未找到${typeName}元素`, "warning"); markFailure(`${typeName}元素未找到`); resolve(); return; } startGuard(); } }, 1000); return; } startGuard(); function startGuard() { setActiveTask(taskMeta); if (isMediaFinished(media)) { touchTaskState(taskMeta, { finished: true, type: taskMeta?.taskType || mediaType }); log(`${typeName}已完成`, "success"); return resolve(); } // 检查是否已处理过 if (__mediaProcessedMap.has(media)) { log(`${typeName}正在由其他任务处理`, "info"); return resolve(); } __mediaProcessedMap.set(media, true); keepAlive.start(); log(`开始播放${typeName}`, "primary"); if (win && !win.__cxHelperAlertHooked) { try { win.alert = () => {}; win.confirm = () => true; win.prompt = () => ""; win.__cxHelperAlertHooked = true; } catch {} } let stuckCount = 0; let lastTime = media.currentTime; let lastLogTime = 0; let finished = false; let timer = null; let timeoutId = null; const finish = (message, type, done = false) => { if (finished) return; finished = true; if (timer) clearInterval(timer); if (timeoutId) clearTimeout(timeoutId); media.removeEventListener("pause", onPause); media.removeEventListener("ended", onEnded); media.removeEventListener("waiting", onWaiting); media.removeEventListener("stalled", onWaiting); media.removeEventListener("error", onWaiting); media.removeEventListener("timeupdate", onProgress); media.removeEventListener("playing", onProgress); media.removeEventListener("seeked", onProgress); __mediaProcessedMap.delete(media); keepAlive.stop(); if (done) { touchTaskState(taskMeta, { finished: true, type: taskMeta?.taskType || mediaType }); } if (message) log(message, type); resolve(); }; const onPause = () => { if (globalPauseState.isPaused) return; tryAutoPlay(media); }; const onEnded = () => { finish(`${typeName}播放结束`, "success", true); }; const onWaiting = () => { if (globalPauseState.isPaused) return; tryAutoPlay(media); }; const onProgress = () => { markTaskProgress(taskMeta); }; media.addEventListener("pause", onPause); media.addEventListener("ended", onEnded); media.addEventListener("waiting", onWaiting); media.addEventListener("stalled", onWaiting); media.addEventListener("error", onWaiting); media.addEventListener("timeupdate", onProgress); media.addEventListener("playing", onProgress); media.addEventListener("seeked", onProgress); tryAutoPlay(media); // 暴力轮询守护(每1.5秒检查一次) timer = setInterval(async () => { // 检查任务失效 if (taskId !== currentTaskId) { finish(); return; } // 检查全局暂停 if (globalPauseState.isPaused) return; // 检查是否结束 if (isMediaFinished(media)) { finish(`${typeName}播放结束`, "success", true); return; } // 强制播放(后台保活核心) if (media.paused) { const now = Date.now(); if (now - lastLogTime > 10000) { // 10秒最多记录一次 log(`${typeName}被暂停,尝试恢复...`, "warning"); lastLogTime = now; } await tryAutoPlay(media); } // 卡顿检测 if (Math.abs(media.currentTime - lastTime) < 0.1) { stuckCount++; if (stuckCount > 8) { // 16秒无进度 log(`${typeName}进度卡顿,尝试恢复`, "danger"); try { media.pause(); await tryAutoPlay(media); } catch {} stuckCount = 0; } } else { stuckCount = 0; lastTime = media.currentTime; markTaskProgress(taskMeta); } }, 1500); // 超时保护:20分钟 timeoutId = setTimeout(() => { if (!isMediaFinished(media)) { finish(`${typeName}处理超时`, "warning"); markFailure(`${typeName}处理超时`); return; } finish(); }, 1200000); } }); }; // 处理章节作业 const processWork = async (iframe, doc, win, addLog, taskMeta) => { const log = addLog || ((message, type = "info") => logStore.addLog(message, type)); touchActivity(); log("处理章节测试...", "info"); if (!doc) return; try { if (isWorkFinishedByDoc(doc)) { log("测试已完成", "success"); touchTaskState(taskMeta, { finished: true, type: taskMeta?.taskType || "work" }); return; } await waitIfPaused(); decrypt(doc); const summary = await new CxQuestionHandler("zj", iframe).init(); if (win) win.alert = () => {}; if (!summary?.total) { if (isWorkFinishedByDoc(doc)) { log("测试已完成", "success"); touchTaskState(taskMeta, { finished: true, type: taskMeta?.taskType || "work" }); touchProgress(); return; } log("未解析到题目,跳过提交", "warning"); return; } let saved = false; if (configStore.chapterSettings.autoSubmit) { const threshold = configStore.otherParams.params[2]?.value ?? 0; const hitRate = summary?.hitRate ?? 0; if (threshold > 0 && hitRate < threshold) { log(`命中率低于 ${threshold}%,暂存答案`, "warning"); try { await win?.noSubmit?.(); } catch {} saved = true; } else { log("自动提交", "success"); try { await win?.btnBlueSubmit?.(); await sleep(1); await win?.submitCheckTimes?.(); } catch {} } } else { log("暂存答案", "info"); try { await win?.noSubmit?.(); } catch {} saved = true; } touchTaskState(taskMeta, { finished: true, saved, type: taskMeta?.taskType || "work" }); touchProgress(); } catch (e) { log("章节测试处理失败", "warning"); markFailure("章节测试处理失败"); } }; const processPpt = async (win, addLog, taskMeta) => { const log = addLog || ((message, type = "info") => logStore.addLog(message, type)); if (!win?.document) return; log("处理文档/PPT任务...", "info"); try { const panView = win.document.querySelector("#panView"); const viewWin = panView?.contentWindow || win; const viewDoc = viewWin?.document; if (viewDoc?.body) { viewWin.scrollTo({ top: viewDoc.body.scrollHeight, behavior: "smooth" }); } log("阅读完成", "success"); touchTaskState(taskMeta, { finished: true, type: taskMeta?.taskType || "doc" }); touchProgress(); } catch (e) { log("文档处理失败", "warning"); markFailure("文档处理失败"); } }; const processBook = async (win, addLog, taskMeta) => { const log = addLog || ((message, type = "info") => logStore.addLog(message, type)); log("处理电子书任务...", "info"); try { if (_unsafeWindow.top?.onchangepage && win?.getFrameAttr) { _unsafeWindow.top.onchangepage(win.getFrameAttr("end")); log("阅读完成", "success"); touchTaskState(taskMeta, { finished: true, type: taskMeta?.taskType || "book" }); touchProgress(); } else { log("未找到电子书翻页接口", "warning"); markFailure("电子书翻页接口缺失"); } } catch (e) { log("电子书处理失败", "warning"); markFailure("电子书处理失败"); } }; // 处理单个 iframe const processIframe = async (iframe, taskId) => { try { await waitIfPaused(); if (taskId !== currentTaskId) return; await waitIframeLoad(iframe); if (taskId !== currentTaskId) return; const src = iframe.src || ""; if (!src || src.includes("javascript:")) return; const doc = safeDoc(iframe); const win = iframe.contentWindow; if (!doc || !win) return; applyDocumentHooks(doc, win); const parent = iframe.parentElement; const hasJobIcon = parent?.querySelector(".ans-job-icon"); const isTask = src.includes("api/work") || hasJobIcon; if (!isTask) return; const { log: taskLog, meta: taskMeta } = createTaskLogger(iframe); const detail = getTaskDetail(iframe); touchTaskState(detail.meta, { finished: detail.finished, saved: detail.saved, type: detail.type }); setActiveTask(taskMeta); if (!iframe.__cxTaskStarted) { taskLog(`正在处理【${taskMeta.taskLabel || "任务点"}】`, "primary"); iframe.__cxTaskStarted = true; } if (detail.saved && detail.type === "work") { taskLog("已暂存,跳过重复答题", "info"); touchTaskState(taskMeta, { saved: true, finished: true, type: taskMeta.taskType }); clearActiveTask(taskMeta); return; } // 跳过已完成 if (detail.finished && configStore.chapterSettings.skipCompleted) { taskLog("跳过已完成任务点", "info"); touchTaskState(taskMeta, { finished: true, type: taskMeta.taskType }); clearActiveTask(taskMeta); return; } if (src.includes("api/work")) { await processWork(iframe, doc, win, taskLog, taskMeta); clearActiveTask(taskMeta); return; } if (configStore.chapterSettings.onlyQuiz) { clearActiveTask(taskMeta); return; } if (hasJobIcon) { if (src.includes("video")) { await processMedia("video", doc, win, taskId, taskLog, taskMeta); } else if (src.includes("audio")) { await processMedia("audio", doc, win, taskId, taskLog, taskMeta); } else if (isDocTask(src)) { await processPpt(win, taskLog, taskMeta); } else if (isBookTask(src)) { await processBook(win, taskLog, taskMeta); } } clearActiveTask(taskMeta); } catch (e) { console.warn("处理iframe出错:", e); markFailure("处理任务点出错"); clearActiveTask(); } }; // 初始化 init(); processIframeTask(); setupInterceptor(); }; // ==================== 17. 作业和考试逻辑 ==================== const useCxWorkLogic = async () => { const logStore = useLogStore(); if (licenseLocked) { logStore.addLog("许可验证未通过,功能已锁定", "danger"); return; } logStore.addLog("作业页面", "primary"); await sleep(2); await new CxQuestionHandler("zy").init(); }; const useCxExamLogic = async () => { const logStore = useLogStore(); const configStore = useConfigStore(); if (licenseLocked) { logStore.addLog("许可验证未通过,功能已锁定", "danger"); return; } logStore.addLog("考试页面", "primary"); await sleep(2); await new CxQuestionHandler("ks").init(); if (configStore.platformParams.cx.parts[1].params[0].value) { await sleep(configStore.otherParams.params[0].value); const currentQuestionNum = parseInt(_unsafeWindow.document.querySelector(".topicNumber_list .current")?.innerText || "0", 10); const totalQuestions = _unsafeWindow.document.querySelectorAll(".topicNumber_list li").length; if (totalQuestions && currentQuestionNum >= totalQuestions) { logStore.addLog("当前已是最后一题,不再自动切换", "warning"); logStore.addLog("请检查答案后手动提交试卷", "primary"); } else { _unsafeWindow.getTheNextQuestion?.(1); } } }; // ==================== 18. Vue 组件 ==================== const ScriptHome = { props: { logList: Array }, setup(props) { const scrollRef = vue.ref(null); const openGroups = vue.ref([]); vue.watch(() => props.logList.length, () => { vue.nextTick(() => scrollRef.value?.setScrollTop?.(99999)); }); const isPaused = vue.computed(() => globalPauseState.isPaused); const entries = vue.computed(() => { const groups = new Map(); const list = []; props.logList.forEach((item, index) => { if (item.taskKey) { const key = item.taskKey; let group = groups.get(key); if (!group) { group = { key, label: item.taskLabel || "任务点", items: [], lastTime: item.time, lastType: item.type }; groups.set(key, group); list.push({ type: "group", group, key: `group-${key}` }); } if (item.taskLabel) { group.label = item.taskLabel; } group.items.push(item); group.lastTime = item.time; group.lastType = item.type; } else { list.push({ type: "log", item, key: `log-${index}` }); } }); return list; }); const isOpen = (key) => openGroups.value.includes(key); const toggleGroup = (key) => { const set = new Set(openGroups.value); if (set.has(key)) set.delete(key); else set.add(key); openGroups.value = Array.from(set); }; const renderLogItem = (item, key) => { return vue.h('div', { key, class: ['log-item', `type-${item.type}`] }, [ vue.h('span', { class: 'log-time' }, item.time), vue.h('span', { class: 'log-message' }, item.message) ]); }; const renderGroupHeader = (group, opened, key) => { const type = group.lastType || "info"; return vue.h('div', { key, class: ['log-item', 'log-group-header', `type-${type}`], onClick: () => toggleGroup(group.key) }, [ vue.h('span', { class: 'log-time' }, group.lastTime || ""), vue.h('span', { class: 'log-message' }, group.label || "任务点"), vue.h('span', { class: 'log-group-count' }, `(${group.items.length})`), vue.h('span', { class: 'log-group-toggle' }, opened ? '收起' : '展开') ]); }; return () => vue.h('div', { class: 'script-log-container', style: { height: '100%' } }, [ vue.h('div', { class: ['status-indicator', isPaused.value ? 'paused' : 'running'] }, [ vue.h('span', { class: 'status-dot' }), vue.h('span', isPaused.value ? '已暂停' : '运行中') ]), vue.h(vue.resolveComponent('el-scrollbar'), { ref: scrollRef, height: "calc(100% - 40px)" }, () => props.logList.length === 0 ? vue.h('div', { class: 'empty-log' }, '暂无日志') : entries.value.map((entry) => { if (entry.type === "log") { return renderLogItem(entry.item, entry.key); } const group = entry.group; const opened = isOpen(group.key); return vue.h('div', { key: entry.key, class: 'log-group' }, [ renderGroupHeader(group, opened, `${entry.key}-header`), opened ? group.items.map((item, index) => renderLogItem(item, `${entry.key}-${index}`)) : null ]); }) ) ]); } }; const ApiManager = { setup() { const configStore = useConfigStore(); const logStore = useLogStore(); const newApiUrl = vue.ref(""); const editingIndex = vue.ref(-1); const editingUrl = vue.ref(""); const addApi = () => { if (newApiUrl.value.trim()) { if (configStore.addApi(newApiUrl.value)) { logStore.addLog("API添加成功", "success"); newApiUrl.value = ""; } else { logStore.addLog("API已存在或格式无效", "warning"); } } }; const startEdit = (index, url) => { editingIndex.value = index; editingUrl.value = url; }; const cancelEdit = () => { editingIndex.value = -1; editingUrl.value = ""; }; const confirmEdit = () => { if (editingUrl.value.trim()) { if (configStore.updateApi(editingIndex.value, editingUrl.value)) { logStore.addLog("API修改成功", "success"); cancelEdit(); } else { logStore.addLog("API已存在或格式无效", "warning"); } } }; const deleteApi = (index) => { if (configStore.removeApi(index)) { logStore.addLog("API删除成功", "success"); if (editingIndex.value === index) { cancelEdit(); } else if (editingIndex.value > index) { editingIndex.value--; } } else { logStore.addLog("至少保留一个API", "warning"); } }; const renderApiItem = (url, index) => { if (editingIndex.value === index) { return vue.h('div', { key: 'edit-' + index, class: 'edit-row' }, [ vue.h(vue.resolveComponent('el-input'), { modelValue: editingUrl.value, 'onUpdate:modelValue': v => editingUrl.value = v, size: "small", placeholder: "输入API地址", autofocus: true, onKeyup: (e) => { if (e.key === 'Enter') confirmEdit(); if (e.key === 'Escape') cancelEdit(); } }), vue.h('div', { class: 'edit-actions' }, [ vue.h('div', { class: 'edit-action-btn confirm', onClick: confirmEdit, title: '确认' }, [ vue.h(vue.resolveComponent('el-icon'), { size: 14 }, () => vue.h(CheckIcon)) ]), vue.h('div', { class: 'edit-action-btn cancel', onClick: cancelEdit, title: '取消' }, [ vue.h(vue.resolveComponent('el-icon'), { size: 14 }, () => vue.h(CloseIcon)) ]) ]) ]); } return vue.h('div', { key: index, class: ['api-item', { active: index === configStore.currentApiIndex }], onClick: () => configStore.selectApi(index), onDblclick: () => startEdit(index, url) }, [ vue.h(vue.resolveComponent('el-icon'), { size: 14, color: index === configStore.currentApiIndex ? '#764ba2' : '#c0c4cc' }, () => vue.h(CheckIcon)), vue.h('span', { class: 'api-url', title: url }, url), vue.h('div', { class: 'action-btns' }, [ vue.h(vue.resolveComponent('el-icon'), { class: 'action-btn', size: 14, color: '#409eff', onClick: (e) => { e.stopPropagation(); startEdit(index, url); }, title: '编辑' }, () => vue.h(EditIcon)), configStore.apiList.length > 1 ? vue.h(vue.resolveComponent('el-icon'), { class: 'action-btn', size: 14, color: '#f56c6c', onClick: (e) => { e.stopPropagation(); deleteApi(index); }, title: '删除' }, () => vue.h(DeleteIcon)) : null ]) ]); }; return () => vue.h('div', { class: 'api-manager', style: { height: '100%' } }, [ vue.h(vue.resolveComponent('el-scrollbar'), { height: "calc(100% - 50px)" }, () => configStore.apiList.length > 0 ? configStore.apiList.map((url, index) => renderApiItem(url, index)) : vue.h('div', { class: 'empty-tip' }, '暂无API,请添加') ), vue.h('div', { class: 'add-api' }, [ vue.h(vue.resolveComponent('el-input'), { modelValue: newApiUrl.value, 'onUpdate:modelValue': v => newApiUrl.value = v, placeholder: "输入API地址 (如: http://localhost:3000)", size: "small", onKeyup: (e) => e.key === 'Enter' && addApi() }), vue.h(vue.resolveComponent('el-button'), { type: "primary", size: "small", onClick: addApi }, () => "添加") ]) ]); } }; const ScriptSetting = { setup() { const configStore = useConfigStore(); const icons = { "章节设置": "📚", "考试设置": "📝", "视频设置": "🎬", "其他参数": "⚙️" }; const platformParams = vue.computed(() => configStore.platformParams); const otherParams = vue.computed(() => configStore.otherParams); const updateParamValue = (partIndex, paramIndex, value) => { configStore.platformParams.cx.parts[partIndex].params[paramIndex].value = value; }; const updateOtherParamValue = (paramIndex, value) => { configStore.otherParams.params[paramIndex].value = value; if (configStore.otherParams.params[paramIndex].name === "许可密钥") { configStore.licenseKey = value; } }; return () => vue.h('div', { class: "script-setting", style: { height: '100%' } }, [ vue.h(vue.resolveComponent('el-scrollbar'), { height: "100%" }, () => [ ...platformParams.value.cx.parts.map((part, partIndex) => vue.h('div', { key: 'part-' + partIndex, class: 'setting-section' }, [ vue.h('div', { class: 'section-title' }, [ vue.h('span', icons[part.name] || '📋'), vue.h('span', part.name) ]), ...part.params.map((param, paramIndex) => vue.h('div', { key: 'param-' + paramIndex, class: 'setting-item' }, [ vue.h('div', [ vue.h('span', { class: 'setting-label' }, param.name), param.tip ? vue.h('div', { class: 'setting-tip' }, param.tip) : null ]), param.type === "boolean" ? vue.h(vue.resolveComponent('el-switch'), { modelValue: param.value, 'onUpdate:modelValue': v => updateParamValue(partIndex, paramIndex, v), size: "small" }) : vue.h(vue.resolveComponent('el-input-number'), { modelValue: param.value, 'onUpdate:modelValue': v => updateParamValue(partIndex, paramIndex, v), min: param.min || 1, max: param.max || 100, size: "small", controlsPosition: "right" }) ]) ) ]) ), vue.h('div', { class: 'setting-section' }, [ vue.h('div', { class: 'section-title' }, [ vue.h('span', icons[otherParams.value.name] || '⚙️'), vue.h('span', otherParams.value.name) ]), ...otherParams.value.params.map((param, index) => vue.h('div', { key: 'other-' + index, class: 'setting-item' }, [ vue.h('div', [ vue.h('span', { class: 'setting-label' }, param.name), param.tip ? vue.h('div', { class: 'setting-tip' }, param.tip) : null ]), param.type === "number" ? vue.h(vue.resolveComponent('el-input-number'), { modelValue: param.value, 'onUpdate:modelValue': v => updateOtherParamValue(index, v), min: param.min || 0, max: param.max || 100, size: "small", controlsPosition: "right" }) : vue.h(vue.resolveComponent('el-input'), { modelValue: param.value, 'onUpdate:modelValue': v => updateOtherParamValue(index, v), size: "small", placeholder: param.tip || "请输入" }) ]) ) ]) ]) ]); } }; const QuestionTable = { props: { questionList: Array }, setup(props) { return () => vue.h('div', { class: 'script-question-table', style: { height: '100%' } }, [ props.questionList.length > 0 ? vue.h(vue.resolveComponent('el-scrollbar'), { height: "100%" }, () => vue.h(vue.resolveComponent('el-table'), { data: props.questionList, stripe: true, size: "small" }, () => [ vue.h(vue.resolveComponent('el-table-column'), { type: "index", width: "40", label: "#" }), vue.h(vue.resolveComponent('el-table-column'), { prop: "title", label: "题目", minWidth: "140", showOverflowTooltip: true }), vue.h(vue.resolveComponent('el-table-column'), { prop: "answer", label: "答案", minWidth: "100" }, { default: scope => vue.h('div', { style: { whiteSpace: 'pre-wrap', wordBreak: 'break-all', fontSize: '11px' }, innerHTML: Array.isArray(scope.row.answer) ? scope.row.answer.join('
') : scope.row.answer }) }) ]) ) : vue.h(vue.resolveComponent('el-empty'), { description: "暂无题目", imageSize: 50 }) ]); } }; const IndexComponent = { setup() { const configStore = useConfigStore(); const logStore = useLogStore(); const questionStore = useQuestionStore(); const tabs = [ { label: "日志", name: "0", component: ScriptHome, props: { logList: logStore.logList } }, { label: "题目", name: "1", component: QuestionTable, props: { questionList: questionStore.questionList } }, { label: "API", name: "2", component: ApiManager }, { label: "设置", name: "3", component: ScriptSetting } ]; return () => vue.h(vue.resolveComponent('el-tabs'), { modelValue: configStore.menuIndex, 'onUpdate:modelValue': v => configStore.menuIndex = v, style: { height: '100%' } }, () => tabs.map(tab => vue.h(vue.resolveComponent('el-tab-pane'), { key: tab.name, label: tab.label, name: tab.name, style: { height: '100%' } }, () => vue.h(tab.component, tab.props)) )); } }; const LayoutComponent = { setup() { const configStore = useConfigStore(); const logStore = useLogStore(); logStore.addLog("脚本启动 v" + getScriptInfo().version, "success"); let logicStarted = false; let licenseCheckTimer = null; const startLogic = () => { const url = window.location.href; const urlLogicPairs = [ { keyword: "/mycourse/studentstudy", logic: useCxChapterLogic }, { keyword: "/mooc2/work/dowork", logic: useCxWorkLogic }, { keyword: "/exam-ans/exam", logic: useCxExamLogic }, { keyword: "mycourse/stu?courseid", logic: () => logStore.addLog("请进入章节或答题页", "warning") } ]; for (const { keyword, logic } of urlLogicPairs) { if (url.includes(keyword)) { logic(); break; } } }; const startLogicOnce = () => { if (logicStarted) return; logicStarted = true; startLogic(); }; const validateLicense = async () => { const api = configStore.currentApiUrl; const licenseKey = configStore.licenseKey || ""; if (!licenseKey) { lockLicense(logStore, "未填写许可密钥,功能已锁定"); return false; } try { await new Promise((resolve, reject) => { _GM_xmlhttpRequest({ url: `${api}/licenses/validate`, method: "POST", headers: { "Content-Type": "application/json" }, data: JSON.stringify({ licenseKey }), timeout: 15000, onload: (resp) => { if (resp.status === 200) resolve(); else reject(new Error(resp.responseText || "许可验证失败")); }, onerror: () => reject(new Error("许可验证请求失败")), ontimeout: () => reject(new Error("许可验证超时")) }); }); logStore.addLog("许可验证通过", "success"); licenseLocked = false; return true; } catch (e) { lockLicense(logStore, `许可验证失败:${e.message}`); return false; } }; const runLicenseValidation = async () => { const ok = await validateLicense(); if (ok) startLogicOnce(); }; const scheduleLicenseValidation = () => { if (licenseCheckTimer) clearTimeout(licenseCheckTimer); licenseCheckTimer = setTimeout(runLicenseValidation, 600); }; runLicenseValidation(); vue.watch(configStore, (newVal) => { const saveConfig = { ...newVal }; _GM_setValue("config", JSON.stringify(saveConfig)); }, { deep: true }); vue.watch( () => [configStore.licenseKey, configStore.currentApiUrl], scheduleLicenseValidation ); const isDragging = vue.ref(false); const offsetX = vue.ref(0); const offsetY = vue.ref(0); const position = vue.ref({ left: configStore.position.x, top: configStore.position.y }); const isPaused = vue.computed(() => globalPauseState.isPaused); const startDrag = (event) => { isDragging.value = true; const rect = event.currentTarget .closest('.main-panel') .getBoundingClientRect(); offsetX.value = event.clientX - rect.left; offsetY.value = event.clientY - rect.top; const onMouseUp = () => { isDragging.value = false; document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); window.removeEventListener("blur", onMouseUp); }; const onMouseMove = (e) => { if (!isDragging.value) return; if (e.buttons !== 1) { onMouseUp(); return; } let x = Math.max(0, Math.min(e.clientX - offsetX.value, window.innerWidth - 360)); let y = Math.max(0, Math.min(e.clientY - offsetY.value, window.innerHeight - 50)); position.value = { left: `${x}px`, top: `${y}px` }; configStore.position.x = `${x}px`; configStore.position.y = `${y}px`; }; window.addEventListener("blur", onMouseUp); document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }; const startCircleDrag = (event) => { if (event.button !== 0) return; event.preventDefault(); let moved = false; isDragging.value = true; const circleEl = event.currentTarget; const rect = circleEl.getBoundingClientRect(); const circleW = rect.width || 20; const circleH = rect.height || 20; offsetX.value = event.clientX - rect.left; offsetY.value = event.clientY - rect.top; if (event.pointerId != null && circleEl.setPointerCapture) { try { circleEl.setPointerCapture(event.pointerId); } catch {} } const cleanup = () => { isDragging.value = false; document.removeEventListener("pointermove", onPointerMove); document.removeEventListener("pointerup", onPointerUp); document.removeEventListener("pointercancel", onPointerUp); window.removeEventListener("blur", onPointerUp); }; const onPointerUp = () => { cleanup(); if (!moved) configStore.isMinus = false; // click toggles展开 }; const onPointerMove = (e) => { if (!isDragging.value || e.buttons !== 1) { cleanup(); return; } moved = true; let x = Math.max(0, Math.min(e.clientX - offsetX.value, window.innerWidth - circleW)); let y = Math.max(0, Math.min(e.clientY - offsetY.value, window.innerHeight - circleH)); position.value = { left: `${x}px`, top: `${y}px` }; configStore.position.x = `${x}px`; configStore.position.y = `${y}px`; }; window.addEventListener("blur", onPointerUp); document.addEventListener("pointermove", onPointerMove); document.addEventListener("pointerup", onPointerUp); document.addEventListener("pointercancel", onPointerUp); }; const handleMinimize = () => { configStore.isMinus = true; }; const handleTogglePause = () => { if (licenseLocked) { logStore.addLog("许可验证未通过,无法解除锁定", "danger"); configStore.setPaused(true); return; } const paused = configStore.togglePause(); logStore.addLog(paused ? '脚本已暂停' : '脚本已继续', paused ? 'warning' : 'success'); }; return () => vue.h('div', { style: position.value, class: "main-page script-container" }, [ configStore.isMinus ? vue.h('div', { class: ['mini-circle', { paused: isPaused.value }], onPointerdown: startCircleDrag, title: '单击展开 / 按住拖动' }) : vue.h('div', { class: 'main-panel' }, [ vue.h(vue.resolveComponent('el-card'), { shadow: "always" }, { header: () => vue.h('div', { class: ["card-header"], onMousedown: startDrag }, [ vue.h('div', { class: "title" }, [ vue.h('span', "🚀"), vue.h('span', configStore.platformParams.cx.name) ]), vue.h('div', { class: 'header-btns' }, [ vue.h('div', { class: ['pause-btn', { playing: isPaused.value }], onClick: (e) => { e.stopPropagation(); handleTogglePause(); }, title: isPaused.value ? '继续' : '暂停' }, [ vue.h('div', { class: 'pause-icon' }) ]), vue.h('div', { class: "minimize-btn", onClick: (e) => { e.stopPropagation(); handleMinimize(); }, title: '最小化' }) ]) ]), default: () => vue.h(IndexComponent) }) ]) ]); } }; const App = { setup() { const configStore = useConfigStore(); configStore.platformName = "cx"; return () => vue.h(LayoutComponent); } }; // ==================== 19. 初始化 ==================== const initApp = () => { if (!isTopWindow) { console.log('[超星自行火炮] 在iframe中运行,跳过UI初始化'); return; } const containerId = 'chaoxing-helper-root'; if (document.getElementById(containerId)) { console.log('[超星自行火炮] 容器已存在,跳过初始化'); return; } const app = vue.createApp(App); app.use(pinia.createPinia()); app.use(ElementPlus); const container = document.createElement("div"); container.id = containerId; document.body.appendChild(container); const shadow = container.attachShadow({ mode: "closed" }); const appDiv = document.createElement("div"); shadow.appendChild(appDiv); const eleStyle = _GM_getResourceText("ElementPlusStyle"); if (eleStyle) { const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(eleStyle); shadow.adoptedStyleSheets = [styleSheet]; } const customSheet = new CSSStyleSheet(); customSheet.replaceSync(customStyles); shadow.adoptedStyleSheets = [...shadow.adoptedStyleSheets, customSheet]; app.mount(appDiv); console.log('[超星自行火炮] UI初始化完成'); }; if (document.readyState === "complete") { initApp(); } else { window.addEventListener('load', initApp, { once: true }); } })(Vue, Pinia, rxjs, md5, ElementPlus);