// ==UserScript== // @name 智慧职教全能助手 // @namespace http://tampermonkey.net/ // @version 3.0.2 // @author caokun // @description 智慧职教MOOC学习助手:仅支持智慧职教MOOC平台,集成自动学习和AI智能答题功能 // @license MIT // @icon https://www.icve.com.cn/favicon.ico // @match https://*.icve.com.cn/excellent-study/* // @match https://*.icve.com.cn/preview-exam/* // @connect * // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const Utils = { /** * 延时函数 * @param ms - 延时毫秒数 */ sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }, /** * 格式化时间(秒转为 MM:SS 格式) * @param seconds - 秒数 */ formatTime(seconds) { if (!seconds || isNaN(seconds) || seconds === Infinity) return "0:00"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; }, /** * 防抖函数 * @param fn - 要防抖的函数 * @param delay - 延时毫秒数 */ debounce(fn, delay = 300) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }, /** * 节流函数 * @param fn - 要节流的函数 * @param delay - 间隔毫秒数 */ throttle(fn, delay = 300) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= delay) { lastTime = now; return fn.apply(this, args); } return void 0; }; }, /** * 安全获取单个 DOM 元素 * @param selector - CSS 选择器 * @param parent - 父元素 */ $(selector, parent = document) { return parent.querySelector(selector); }, /** * 安全获取多个 DOM 元素 * @param selector - CSS 选择器 * @param parent - 父元素 */ $$(selector, parent = document) { return Array.from(parent.querySelectorAll(selector)); }, /** * 带重试的异步操作 * @param fn - 要执行的异步函数 * @param maxRetries - 最大重试次数 * @param delay - 重试间隔毫秒数 */ async retry(fn, maxRetries = 3, delay = 1e3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; await this.sleep(delay); } } throw new Error("Retry failed"); }, /** * 更新进度条(带智能标签定位) * @param progressBarId - 进度条元素 ID * @param percentage - 百分比 (0-100) */ updateProgressBar(progressBarId, percentage) { const progressBar = document.getElementById(progressBarId); if (!progressBar) return; percentage = Math.max(0, Math.min(100, percentage)); const roundedPercentage = Math.round(percentage); progressBar.style.width = `${percentage}%`; progressBar.setAttribute("data-progress", `${roundedPercentage}%`); if (percentage > 70) { progressBar.classList.add("progress-label-inside"); } else { progressBar.classList.remove("progress-label-inside"); } }, /** * 重置进度条 * @param progressBarId - 进度条元素 ID */ resetProgressBar(progressBarId) { const progressBar = document.getElementById(progressBarId); if (!progressBar) return; progressBar.style.width = "0%"; progressBar.setAttribute("data-progress", "0%"); progressBar.classList.remove("progress-label-inside"); } }; const Logger = { _prefix: "[智慧职教助手]", _maxLogs: 100, _logs: [], _log(level, ...args) { const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString(); this._addPageLog(level, timestamp, args); }, _addPageLog(level, timestamp, args) { const message = args.map( (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg) ).join(" "); const logEntry = { type: level, time: timestamp, message, id: Date.now() + Math.random() }; this._logs.push(logEntry); if (this._logs.length > this._maxLogs) { this._logs.shift(); } if (typeof window.updateRecentEvents === "function") { window.updateRecentEvents(); } }, clearPageLog() { this._logs = []; if (typeof window.updateRecentEvents === "function") { window.updateRecentEvents(); } }, info(...args) { this._log("info", ...args); }, success(...args) { this._log("success", ...args); }, warn(...args) { this._log("warn", ...args); }, error(...args) { this._log("error", ...args); } }; const AI_PRESETS = { openai: { name: "OpenAI", baseURL: "https://api.openai.com/v1", model: "gpt-4o-mini", defaultKey: "", keyPlaceholder: "sk-xxx" }, deepseek: { name: "DeepSeek", baseURL: "https://api.deepseek.com/v1", model: "deepseek-chat", defaultKey: "", keyPlaceholder: "sk-xxx" }, custom: { name: "自定义", baseURL: "https://api.openai.com/v1", model: "gpt-4o-mini", defaultKey: "", keyPlaceholder: "sk-xxx" } }; function normalizeAIType(aiType) { return AI_PRESETS[aiType] ? aiType : "custom"; } const ConfigManager = { keys: { learning: { playbackRate: "learning_playbackRate", waitTimeAfterComplete: "learning_waitTime", documentPageInterval: "learning_docInterval", expandDelay: "learning_expandDelay", muteMedia: "learning_muteMedia" }, exam: { delay: "exam_delay", autoSubmit: "exam_autoSubmit", currentAI: "exam_currentAI" }, progress: { processedNodes: "learning_processedNodes", completedChapters: "learning_completedChapters" } }, defaults: { learning: { playbackRate: 1, waitTimeAfterComplete: 2, documentPageInterval: 1, expandDelay: 3, muteMedia: false }, exam: { delay: 3e3, autoSubmit: false, currentAI: "custom" } }, get(category, key) { const categoryKeys = this.keys[category]; const categoryDefaults = this.defaults[category]; const storageKey = categoryKeys == null ? void 0 : categoryKeys[key]; const defaultValue = categoryDefaults == null ? void 0 : categoryDefaults[key]; if (storageKey) { const value = GM_getValue(storageKey, defaultValue); if (category === "exam" && key === "currentAI") { return normalizeAIType(String(value)); } return value; } return defaultValue; }, saveAll(config) { if (config.learning) { const learningKeys = this.keys.learning; Object.keys(learningKeys).forEach((key) => { var _a; const value = (_a = config.learning) == null ? void 0 : _a[key]; if (value !== void 0) { GM_setValue(learningKeys[key], value); } }); } if (config.exam) { const examKeys = this.keys.exam; Object.keys(examKeys).forEach((key) => { var _a; let value = (_a = config.exam) == null ? void 0 : _a[key]; if (value !== void 0) { if (key === "currentAI") { value = normalizeAIType(String(value)); } GM_setValue(examKeys[key], value); } }); } if (config.theme) { localStorage.setItem("icve_theme_mode", config.theme); } }, getAIConfig(aiType) { const normalizedAIType = normalizeAIType(aiType); const preset = AI_PRESETS[normalizedAIType]; return { apiKey: GM_getValue(`ai_key_${normalizedAIType}`, preset.defaultKey), baseURL: GM_getValue(`ai_baseurl_${normalizedAIType}`, preset.baseURL), model: GM_getValue(`ai_model_${normalizedAIType}`, preset.model) }; } }; class ReactiveStateManager { constructor() { this._state = null; this._initialized = false; } /** * 初始化状态 */ init() { if (this._initialized && this._state) { return this._state; } const initialState = { learning: { isRunning: false, currentNode: null, allNodes: [], completedCount: 0, totalCount: 0, examCount: 0, processedNodes: new Set(GM_getValue(ConfigManager.keys.progress.processedNodes, [])), completedChapters: new Set(GM_getValue(ConfigManager.keys.progress.completedChapters, [])), currentPage: 1, totalPages: 1, isDocument: false, mediaWatching: false }, exam: { isRunning: false, currentQuestionIndex: 0, totalQuestions: 0 } }; this._state = initialState; this._initialized = true; return this._state; } } const stateManager = new ReactiveStateManager(); const state = stateManager.init(); function saveLearningProgress() { GM_setValue(ConfigManager.keys.progress.processedNodes, Array.from(state.learning.processedNodes)); GM_setValue(ConfigManager.keys.progress.completedChapters, Array.from(state.learning.completedChapters)); } function loadLearningProgress() { const processedNodes = GM_getValue(ConfigManager.keys.progress.processedNodes, []); const completedChapters = GM_getValue(ConfigManager.keys.progress.completedChapters, []); state.learning.processedNodes = new Set(processedNodes); state.learning.completedChapters = new Set(completedChapters); } const CONFIG = { learning: { playbackRate: ConfigManager.get("learning", "playbackRate"), waitTimeAfterComplete: ConfigManager.get("learning", "waitTimeAfterComplete"), documentPageInterval: ConfigManager.get("learning", "documentPageInterval"), expandDelay: ConfigManager.get("learning", "expandDelay"), muteMedia: ConfigManager.get("learning", "muteMedia") }, exam: { delay: ConfigManager.get("exam", "delay"), autoSubmit: ConfigManager.get("exam", "autoSubmit"), currentAI: ConfigManager.get("exam", "currentAI") }, theme: localStorage.getItem("icve_theme_mode") || "light", currentTab: "learning" }; function saveConfig() { ConfigManager.saveAll(CONFIG); } const refactorStyles = ` #icve-tabbed-panel { --ui-bg-root: #f7f9fc; --ui-bg-surface: #ffffff; --ui-bg-subtle: #f2f6fb; --ui-bg-hover: #eaf2fb; --ui-border: #dbe5f0; --ui-border-strong: #bfd0e3; --ui-text: #1f2a3d; --ui-text-muted: #66758a; --ui-text-subtle: #9aa8ba; --ui-primary: #3b82f6; --ui-primary-soft: #dbeafe; --ui-primary-hover: #2563eb; --ui-success: #10b981; --ui-success-soft: #d1fae5; --ui-warning: #f59e0b; --ui-warning-soft: #fef3c7; --ui-danger: #ef4444; --ui-danger-soft: #fee2e2; --ui-shadow: 0 14px 36px rgba(31, 42, 61, 0.12); position: fixed; top: 24px; right: 24px; width: 430px; max-width: calc(100vw - 32px); z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; color: var(--ui-text); } #icve-tabbed-panel.dark-theme { --ui-bg-root: #0b1020; --ui-bg-surface: #111827; --ui-bg-subtle: #182235; --ui-bg-hover: #223047; --ui-border: #2c3a51; --ui-border-strong: #3e4f68; --ui-text: #e7eef9; --ui-text-muted: #a8b4c7; --ui-text-subtle: #718198; --ui-primary: #60a5fa; --ui-primary-soft: rgba(96, 165, 250, 0.16); --ui-primary-hover: #93c5fd; --ui-success: #34d399; --ui-success-soft: rgba(52, 211, 153, 0.16); --ui-warning: #fbbf24; --ui-warning-soft: rgba(251, 191, 36, 0.16); --ui-danger: #f87171; --ui-danger-soft: rgba(248, 113, 113, 0.16); --ui-shadow: 0 18px 48px rgba(0, 0, 0, 0.42); } #icve-tabbed-panel, #icve-tabbed-panel * { box-sizing: border-box; letter-spacing: 0; } #icve-tabbed-panel .icve-launcher { display: flex; align-items: center; gap: 10px; min-width: 264px; min-height: 52px; max-width: min(308px, calc(100vw - 32px)); margin-left: auto; padding: 0 16px 0 12px; border: 1px solid #b7cce2; border-radius: 999px; background: var(--ui-bg-surface); color: var(--ui-text); box-shadow: 0 16px 38px rgba(31, 42, 61, 0.16), 0 0 0 4px rgba(59, 130, 246, 0.08); cursor: grab; font: inherit; user-select: none; touch-action: none; transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease; } #icve-tabbed-panel .icve-launcher:hover { transform: translateY(-2px); border-color: #8eb1d6; background: #f8fbff; box-shadow: 0 18px 42px rgba(31, 42, 61, 0.18), 0 0 0 5px rgba(59, 130, 246, 0.12); } #icve-tabbed-panel .icve-launcher:active { cursor: grabbing; transform: translateY(0); } #icve-tabbed-panel .launcher-grip { width: 14px; height: 28px; flex: 0 0 auto; border-radius: 999px; background: radial-gradient(circle, #8aa3bf 1.2px, transparent 1.4px) 1px 2px / 6px 6px, radial-gradient(circle, #8aa3bf 1.2px, transparent 1.4px) 5px 2px / 6px 6px; opacity: 0.9; } #icve-tabbed-panel .launcher-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ui-success); flex: 0 0 auto; box-shadow: 0 0 0 4px var(--ui-success-soft); } #icve-tabbed-panel .launcher-dot.running { background: var(--ui-primary); } #icve-tabbed-panel .launcher-dot.error { background: var(--ui-danger); } #icve-tabbed-panel .launcher-title { font-size: 14px; font-weight: 700; white-space: nowrap; } #icve-tabbed-panel .launcher-meta { padding-left: 4px; font-size: 12px; color: var(--ui-text-muted); white-space: nowrap; } #icve-tabbed-panel.is-open .icve-launcher { display: none; } #icve-tabbed-panel.is-collapsed { width: max-content; } #icve-tabbed-panel.is-collapsed .icve-launcher { display: flex; } #icve-tabbed-panel.is-collapsed .panel-container { display: none; } #icve-tabbed-panel .panel-container { width: 100%; max-height: min(820px, calc(100vh - 48px)); overflow: hidden; display: flex; flex-direction: column; background: var(--ui-bg-surface); border: 1px solid var(--ui-border); border-radius: 16px; box-shadow: var(--ui-shadow); backdrop-filter: none; -webkit-backdrop-filter: none; position: relative; } #icve-tabbed-panel .panel-container::before, #icve-tabbed-panel .panel-header::before, #icve-tabbed-panel .panel-header::after { display: none; } #icve-tabbed-panel .panel-header { height: 54px; padding: 0 14px 0 16px; display: flex; align-items: center; justify-content: space-between; cursor: move; user-select: none; background: var(--ui-bg-surface); border-bottom: 1px solid var(--ui-border); position: relative; z-index: 3; } #icve-tabbed-panel .panel-title { color: var(--ui-text); font-size: 15px; font-weight: 750; text-shadow: none; } #icve-tabbed-panel .panel-title::before { display: none; } #icve-tabbed-panel .header-controls { display: flex; align-items: center; gap: 6px; } #icve-tabbed-panel .theme-toggle, #icve-tabbed-panel .panel-toggle { min-width: 32px; height: 32px; padding: 0; border-radius: 9px; border: 1px solid var(--ui-border); background: var(--ui-bg-subtle); color: var(--ui-text-muted); box-shadow: none; cursor: pointer; font-size: 12px; font-weight: 700; transform: none; } #icve-tabbed-panel .theme-toggle { min-width: 42px; } #icve-tabbed-panel .theme-toggle:hover, #icve-tabbed-panel .panel-toggle:hover { background: var(--ui-bg-hover); border-color: var(--ui-border-strong); color: var(--ui-text); transform: none; } #icve-tabbed-panel .workbench-body { padding: 14px; overflow-y: auto; max-height: calc(min(820px, 100vh - 48px) - 54px); background: var(--ui-bg-root); } #icve-tabbed-panel .task-switcher { display: grid; grid-template-columns: repeat(var(--task-count, 3), minmax(0, 1fr)); gap: 4px; padding: 4px; margin-bottom: 12px; background: var(--ui-bg-subtle); border: 1px solid var(--ui-border); border-radius: 12px; } #icve-tabbed-panel .task-switch-btn { height: 32px; border: 0; border-radius: 8px; background: transparent; color: var(--ui-text-muted); cursor: pointer; font: inherit; font-size: 13px; font-weight: 700; } #icve-tabbed-panel .task-switch-btn.active { background: var(--ui-bg-surface); color: var(--ui-primary); box-shadow: 0 1px 3px rgba(31, 42, 61, 0.08); } #icve-tabbed-panel .workbench-task[hidden] { display: none; } #icve-tabbed-panel .task-card, #icve-tabbed-panel .recent-events, #icve-tabbed-panel .config-card { background: var(--ui-bg-surface); border: 1px solid var(--ui-border); border-radius: 14px; box-shadow: none; } #icve-tabbed-panel .task-card { padding: 14px; } #icve-tabbed-panel .task-card-header { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 12px; } #icve-tabbed-panel .section-kicker, #icve-tabbed-panel .metric-label { color: var(--ui-text-subtle); font-size: 11px; font-weight: 700; text-transform: uppercase; } #icve-tabbed-panel .task-card h2 { margin: 3px 0 0; font-size: 20px; line-height: 1.2; color: var(--ui-text); } #icve-tabbed-panel .task-status { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 0 9px; border-radius: 999px; background: var(--ui-bg-subtle); color: var(--ui-text-muted); font-size: 12px; font-weight: 700; } #icve-tabbed-panel .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ui-text-subtle); box-shadow: none; } #icve-tabbed-panel .status-dot.running { background: var(--ui-success); box-shadow: none; animation: none; } #icve-tabbed-panel .status-dot.completed { background: var(--ui-primary); box-shadow: none; } #icve-tabbed-panel .task-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; } #icve-tabbed-panel .task-metrics > div, #icve-tabbed-panel .quick-settings > div { min-width: 0; padding: 10px; background: var(--ui-bg-subtle); border-radius: 10px; } #icve-tabbed-panel .task-metrics strong, #icve-tabbed-panel .quick-settings strong { display: block; margin-top: 3px; color: var(--ui-text); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #icve-tabbed-panel .progress-bar-wrapper { height: 8px; margin: 0 0 12px; background: var(--ui-bg-subtle); border-radius: 999px; box-shadow: none; overflow: hidden; } #icve-tabbed-panel .progress-bar { height: 100%; border-radius: inherit; background: var(--ui-primary); box-shadow: none; } #icve-tabbed-panel .progress-bar::before, #icve-tabbed-panel .progress-bar::after { display: none; } #icve-tabbed-panel .current-line { display: flex; justify-content: space-between; gap: 12px; align-items: center; padding: 9px 0; color: var(--ui-text-muted); border-top: 1px solid var(--ui-border); font-size: 12px; } #icve-tabbed-panel .current-line strong { min-width: 0; color: var(--ui-text); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #icve-tabbed-panel .inline-status { padding-top: 8px; color: var(--ui-text-muted); font-size: 12px; line-height: 1.45; } #icve-tabbed-panel .primary-action-block { margin-top: 12px; } #icve-tabbed-panel .action-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; } #icve-tabbed-panel .btn { min-height: 38px; padding: 0 12px; border-radius: 10px; border: 1px solid transparent; box-shadow: none; cursor: pointer; font: inherit; font-size: 13px; font-weight: 750; transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, transform 0.12s ease; } #icve-tabbed-panel .btn::before, #icve-tabbed-panel .btn::after { display: none; } #icve-tabbed-panel .btn:hover:not(:disabled) { transform: translateY(-1px); } #icve-tabbed-panel .btn:disabled { opacity: 0.48; cursor: not-allowed; transform: none; filter: none; } #icve-tabbed-panel .btn-primary, #icve-tabbed-panel .btn-large { width: 100%; height: 42px; background: var(--ui-primary); border-color: var(--ui-primary); color: #ffffff !important; } #icve-tabbed-panel .btn-primary:hover:not(:disabled), #icve-tabbed-panel .btn-large:hover:not(:disabled) { background: var(--ui-primary-hover); } #icve-tabbed-panel .btn-secondary { background: var(--ui-bg-subtle); color: var(--ui-text); border-color: var(--ui-border); } #icve-tabbed-panel .btn-outline { background: var(--ui-bg-surface) !important; color: var(--ui-text) !important; border-color: var(--ui-border) !important; } #icve-tabbed-panel .btn-danger { background: var(--ui-danger-soft); color: var(--ui-danger); border-color: transparent; } #icve-tabbed-panel .quick-settings { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 12px; } #icve-tabbed-panel .quick-settings span { color: var(--ui-text-subtle); font-size: 11px; font-weight: 700; } #icve-tabbed-panel .recent-events { padding: 12px; margin-top: 12px; min-height: 174px; } #icve-tabbed-panel .recent-events-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } #icve-tabbed-panel .recent-clear-btn { height: 24px; padding: 0 9px; border: 1px solid var(--ui-border); border-radius: 7px; background: var(--ui-bg-subtle); color: var(--ui-text-muted); cursor: pointer; font: inherit; font-size: 11px; font-weight: 750; } #icve-tabbed-panel .recent-clear-btn:hover { border-color: var(--ui-border-strong); background: var(--ui-bg-hover); color: var(--ui-text); } #icve-tabbed-panel[data-task="config"] .recent-events { display: none; } #icve-tabbed-panel #recent-events-list { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; max-height: 132px; overflow-y: auto; padding-right: 2px; } #icve-tabbed-panel .recent-event { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 8px; align-items: start; color: var(--ui-text-muted); font-size: 12px; min-height: 18px; } #icve-tabbed-panel .recent-event strong { min-width: 0; color: var(--ui-text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #icve-tabbed-panel .recent-placeholder { color: var(--ui-text-subtle); font-size: 12px; } #icve-tabbed-panel[data-task="config"] .workbench-body { overflow-y: hidden; } #icve-tabbed-panel .config-card { padding: 10px 12px 12px; } #icve-tabbed-panel .config-card-head { display: flex; justify-content: space-between; gap: 10px; align-items: center; margin-bottom: 8px; } #icve-tabbed-panel .config-card h2 { margin: 0; color: var(--ui-text); font-size: 15px; line-height: 1.2; } #icve-tabbed-panel .config-pill { flex: 0 0 auto; max-width: 158px; padding: 4px 8px; border-radius: 999px; background: var(--ui-bg-subtle); color: var(--ui-primary); font-size: 11px; font-weight: 750; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #icve-tabbed-panel .config-rows { display: flex; flex-direction: column; gap: 7px; } #icve-tabbed-panel .config-row { display: grid; grid-template-columns: 42px minmax(0, 1fr); gap: 8px; align-items: start; padding-top: 7px; border-top: 1px solid var(--ui-border); } #icve-tabbed-panel .config-row-title { padding-top: 19px; color: var(--ui-text-subtle); font-size: 11px; font-weight: 750; } #icve-tabbed-panel .config-fields { display: grid; gap: 7px; align-items: end; min-width: 0; } #icve-tabbed-panel .config-fields-learning { grid-template-columns: 0.9fr repeat(3, 0.78fr) 54px; } #icve-tabbed-panel .config-fields-exam { grid-template-columns: 112px 54px; justify-content: start; } #icve-tabbed-panel .config-row-actions { align-items: center; } #icve-tabbed-panel .config-row-actions .config-row-title { padding-top: 0; } #icve-tabbed-panel .config-fields-ai { grid-template-columns: 0.95fr 1.05fr; } #icve-tabbed-panel .field-full { grid-column: 1 / -1; } #icve-tabbed-panel .field { display: flex; flex-direction: column; gap: 4px; } #icve-tabbed-panel .field { min-width: 0; margin-bottom: 0; } #icve-tabbed-panel .field > span, #icve-tabbed-panel .toggle-row > span { color: var(--ui-text-muted); font-size: 11px; font-weight: 700; } #icve-tabbed-panel .select-control, #icve-tabbed-panel .input-control { width: 100%; height: 28px; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: 8px; background: var(--ui-bg-surface); color: var(--ui-text); font: inherit; font-size: 12px; box-shadow: none; outline: none; } #icve-tabbed-panel .select-control:focus, #icve-tabbed-panel .input-control:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-soft); } #icve-tabbed-panel .input-with-unit { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; padding: 0 7px 0 0; border: 1px solid var(--ui-border); border-radius: 8px; background: var(--ui-bg-surface); } #icve-tabbed-panel .input-with-unit .input-control { border: 0; box-shadow: none; background: transparent; } #icve-tabbed-panel .input-with-unit .unit { color: var(--ui-text-subtle); font-size: 11px; font-weight: 700; } #icve-tabbed-panel .toggle-row { min-height: 28px; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0; min-width: 0; } #icve-tabbed-panel .toggle-field { align-items: stretch; } #icve-tabbed-panel .toggle-field input[type="checkbox"] { margin: 0; } #icve-tabbed-panel .toggle-row input[type="checkbox"], #icve-tabbed-panel .toggle-field input[type="checkbox"] { appearance: none; -webkit-appearance: none; width: 48px; height: 28px; flex: 0 0 auto; position: relative; border: 1px solid var(--ui-border-strong); border-radius: 999px; background: var(--ui-bg-subtle); cursor: pointer; outline: none; transition: background 0.18s ease, border-color 0.18s ease; } #icve-tabbed-panel .toggle-row input[type="checkbox"]::before, #icve-tabbed-panel .toggle-field input[type="checkbox"]::before { content: ''; position: absolute; width: 22px; height: 22px; left: 2px; top: 2px; border-radius: 50%; background: var(--ui-bg-surface); box-shadow: 0 1px 2px rgba(31, 42, 61, 0.18); transition: transform 0.18s ease; } #icve-tabbed-panel .toggle-row input[type="checkbox"]:checked, #icve-tabbed-panel .toggle-field input[type="checkbox"]:checked { border-color: var(--ui-primary); background: var(--ui-primary); } #icve-tabbed-panel .toggle-row input[type="checkbox"]:checked::before, #icve-tabbed-panel .toggle-field input[type="checkbox"]:checked::before { transform: translateX(20px); } #icve-tabbed-panel .toggle-row input[type="checkbox"]:focus-visible, #icve-tabbed-panel .toggle-field input[type="checkbox"]:focus-visible { box-shadow: 0 0 0 3px var(--ui-primary-soft); } #icve-tabbed-panel .api-key-input-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 42px; gap: 5px; align-items: center; } #icve-tabbed-panel .api-key-toggle { height: 28px; border: 1px solid var(--ui-border); border-radius: 8px; background: var(--ui-bg-subtle); color: var(--ui-text-muted); font-size: 11px; font-weight: 750; cursor: pointer; } #icve-tabbed-panel .config-test-cell { display: grid; grid-column: 1 / -1; grid-template-columns: 58px minmax(0, 1fr); gap: 5px; align-items: stretch; } #icve-tabbed-panel .config-test-action { width: 100%; min-height: 28px; padding: 0 8px; border-radius: 8px; } #icve-tabbed-panel .config-test-status { min-width: 0; min-height: 28px; display: flex; align-items: center; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: 8px; background: var(--ui-bg-subtle); color: var(--ui-text-muted); font-size: 11px; line-height: 1.35; font-weight: 750; overflow: visible; white-space: normal; word-break: break-word; } #icve-tabbed-panel .config-test-status.is-pending { color: var(--ui-primary); border-color: var(--ui-primary-soft); background: var(--ui-primary-soft); } #icve-tabbed-panel .config-test-status.is-success { color: var(--ui-success); border-color: var(--ui-success-soft); background: var(--ui-success-soft); } #icve-tabbed-panel .config-test-status.is-error { color: var(--ui-danger); border-color: var(--ui-danger-soft); background: var(--ui-danger-soft); } #icve-tabbed-panel .config-actions, #icve-tabbed-panel .config-action-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; } #icve-tabbed-panel .config-actions .btn, #icve-tabbed-panel .config-action-row .btn { min-height: 28px; height: 28px; padding: 0 8px; border-radius: 8px; font-size: 12px; } @media (max-width: 480px) { #icve-tabbed-panel { top: 16px; right: 16px; width: calc(100vw - 32px); } #icve-tabbed-panel .panel-container { max-height: calc(100vh - 32px); } #icve-tabbed-panel .workbench-body { max-height: calc(100vh - 86px); } #icve-tabbed-panel .quick-settings { grid-template-columns: repeat(2, 1fr); } #icve-tabbed-panel[data-task="config"] .workbench-body { overflow-y: auto; } #icve-tabbed-panel .config-row, #icve-tabbed-panel .config-fields, #icve-tabbed-panel .config-fields-learning, #icve-tabbed-panel .config-fields-exam, #icve-tabbed-panel .config-fields-ai { grid-template-columns: 1fr; } #icve-tabbed-panel .config-row-title { padding-top: 0; } #icve-tabbed-panel .field-full { grid-column: auto; } #icve-tabbed-panel .config-actions, #icve-tabbed-panel .config-action-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } } `; function addStyles() { const style = document.createElement("style"); style.id = "icve-helper-styles"; style.textContent = refactorStyles; document.head.appendChild(style); } const GUIDE_STORAGE_KEY = "icve_guide_completed"; const GUIDE_VERSION = "1.0"; const guideSteps = [ { title: "👋 欢迎使用智慧职教全能助手", content: "本助手可以帮助您自动学习课程和智能答题。让我们快速了解一下主要功能!" }, { title: "📚 自动学习功能", content: '在学习页面,点击"开始学习"即可自动播放视频、浏览文档。播放倍速、完成等待和静音模式可在"配置"中调整。', target: '[data-task-panel="learning"]' }, { title: "🤖 AI智能答题", content: '在答题页面,先到"配置"里设置服务、模型和 API Key,再点击"开始答题"即可自动答题。', target: '[data-task-panel="exam"]' }, { title: "📋 日志查看", content: "学习和答题页面下方会显示最近操作,方便快速确认执行状态。", target: ".recent-events" }, { title: "⚙️ 小技巧", content: "• 拖动标题栏或折叠入口可移动面板\n• 学习、答题和配置是同级标签\n• 所有配置会自动保存\n• 面板位置和折叠状态也会记住" }, { title: "✅ 开始使用", content: "现在您已经了解了基本功能,开始您的学习之旅吧!如有问题请查看GitHub项目页面。" } ]; function shouldShowGuide() { try { const stored = localStorage.getItem(GUIDE_STORAGE_KEY); if (!stored) return true; const data = JSON.parse(stored); return data.version !== GUIDE_VERSION; } catch { return true; } } function markGuideCompleted() { localStorage.setItem(GUIDE_STORAGE_KEY, JSON.stringify({ version: GUIDE_VERSION, completedAt: Date.now() })); } function resetGuide() { localStorage.removeItem(GUIDE_STORAGE_KEY); } function createGuideModal() { const modal = document.createElement("div"); modal.id = "icve-guide-modal"; const panel = document.getElementById("icve-tabbed-panel"); modal.className = (panel == null ? void 0 : panel.classList.contains("dark-theme")) ? "guide-modal dark-theme" : "guide-modal"; modal.innerHTML = `
1 / ${guideSteps.length}

${guideSteps[0].title}

${guideSteps[0].content.replace(/\n/g, "
")}

`; return modal; } function showGuide() { var _a, _b, _c, _d; if (!shouldShowGuide()) return; const modal = createGuideModal(); document.body.appendChild(modal); let currentStep = 0; const updateStep = (step) => { currentStep = step; const stepData = guideSteps[step]; const title = modal.querySelector(".guide-title"); const text = modal.querySelector(".guide-text"); const indicator = modal.querySelector(".guide-step-indicator"); const prevBtn = modal.querySelector(".guide-prev"); const nextBtn = modal.querySelector(".guide-next"); const dots = modal.querySelectorAll(".guide-dot"); if (title) title.textContent = stepData.title; if (text) text.innerHTML = stepData.content.replace(/\n/g, "
"); if (indicator) indicator.textContent = `${step + 1} / ${guideSteps.length}`; if (prevBtn) prevBtn.disabled = step === 0; if (nextBtn) { nextBtn.textContent = step === guideSteps.length - 1 ? "完成" : "下一步"; } dots.forEach((dot, i) => { dot.classList.toggle("active", i === step); }); highlightTarget(stepData.target); }; const highlightTarget = (selector) => { document.querySelectorAll(".guide-highlight").forEach((el) => { el.classList.remove("guide-highlight"); }); if (selector) { const target = document.querySelector(selector); if (target) { target.classList.add("guide-highlight"); if (selector.startsWith("#tab-")) { const tabName = selector.replace("#tab-", ""); const tabBtn = document.querySelector(`[data-tab="${tabName}"]`); if (tabBtn) tabBtn.click(); } } } }; const closeGuide = () => { document.querySelectorAll(".guide-highlight").forEach((el) => { el.classList.remove("guide-highlight"); }); modal.remove(); markGuideCompleted(); }; (_a = modal.querySelector(".guide-close")) == null ? void 0 : _a.addEventListener("click", closeGuide); (_b = modal.querySelector(".guide-overlay")) == null ? void 0 : _b.addEventListener("click", closeGuide); (_c = modal.querySelector(".guide-prev")) == null ? void 0 : _c.addEventListener("click", () => { if (currentStep > 0) updateStep(currentStep - 1); }); (_d = modal.querySelector(".guide-next")) == null ? void 0 : _d.addEventListener("click", () => { if (currentStep < guideSteps.length - 1) { updateStep(currentStep + 1); } else { closeGuide(); } }); modal.querySelectorAll(".guide-dot").forEach((dot) => { dot.addEventListener("click", (e) => { const step = parseInt(e.target.dataset.step || "0"); updateStep(step); }); }); const handleKeydown = (e) => { if (e.key === "Escape") { closeGuide(); } else if (e.key === "ArrowRight" || e.key === "Enter") { if (currentStep < guideSteps.length - 1) { updateStep(currentStep + 1); } else { closeGuide(); } } else if (e.key === "ArrowLeft") { if (currentStep > 0) updateStep(currentStep - 1); } }; document.addEventListener("keydown", handleKeydown); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.removedNodes.forEach((node) => { if (node === modal) { document.removeEventListener("keydown", handleKeydown); observer.disconnect(); } }); }); }); observer.observe(document.body, { childList: true }); } function getGuideStyles() { return ` /* 引导模态框 */ .guide-modal { --guide-bg: #ffffff; --guide-bg-subtle: #f7f9fc; --guide-bg-hover: #eaf2fb; --guide-border: #dbe5f0; --guide-border-strong: #bfd0e3; --guide-text: #1f2a3d; --guide-text-muted: #66758a; --guide-primary: #3b82f6; --guide-primary-hover: #2563eb; --guide-primary-soft: #dbeafe; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 100001; display: flex; align-items: center; justify-content: center; } .guide-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(31, 42, 61, 0.18); backdrop-filter: none; } .guide-content { position: relative; background: var(--guide-bg); border: 1px solid var(--guide-border); border-radius: 14px; box-shadow: 0 18px 42px rgba(31, 42, 61, 0.16); width: 90%; max-width: 460px; overflow: hidden; animation: guideSlideIn 0.3s ease; } @keyframes guideSlideIn { from { opacity: 0; transform: translateY(-20px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } .guide-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--guide-bg); color: var(--guide-text); border-bottom: 1px solid var(--guide-border); } .guide-step-indicator { font-size: 13px; color: var(--guide-text-muted); opacity: 1; } .guide-close { width: 32px; height: 32px; background: var(--guide-bg-subtle); border: 1px solid var(--guide-border); border-radius: 9px; color: var(--guide-text-muted); font-size: 14px; cursor: pointer; opacity: 1; transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease; padding: 0; } .guide-close:hover { background: var(--guide-bg-hover); border-color: var(--guide-border-strong); color: var(--guide-text); } .guide-body { padding: 24px; } .guide-title { margin: 0 0 12px 0; font-size: 18px; color: var(--guide-text); } .guide-text { margin: 0; font-size: 14px; color: var(--guide-text-muted); line-height: 1.6; } .guide-footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 16px 24px; background: var(--guide-bg-subtle); border-top: 1px solid var(--guide-border); } .guide-footer .btn { min-width: 86px; height: 38px; padding: 0 14px; border-radius: 10px; font-size: 13px; font-weight: 700; box-shadow: none; } .guide-footer .guide-prev { background: var(--guide-bg) !important; border: 1px solid var(--guide-border) !important; color: var(--guide-text-muted) !important; } .guide-footer .guide-prev:not(:disabled):hover { background: var(--guide-bg-hover) !important; border-color: var(--guide-border-strong) !important; color: var(--guide-text) !important; } .guide-footer .guide-prev:disabled { background: #f2f6fb !important; border-color: #e3ebf5 !important; color: #a6b3c4 !important; opacity: 1; cursor: not-allowed; } .guide-footer .guide-next { background: var(--guide-primary) !important; border: 1px solid var(--guide-primary) !important; color: white !important; } .guide-footer .guide-next:hover { background: var(--guide-primary-hover) !important; border-color: var(--guide-primary-hover) !important; filter: none; } .guide-dots { display: flex; gap: 8px; } .guide-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--guide-border-strong); cursor: pointer; transition: all 0.2s; } .guide-dot:hover { background: var(--guide-text-subtle, #9aa8ba); } .guide-dot.active { background: var(--guide-primary); transform: scale(1.2); } /* 高亮目标元素 */ .guide-highlight { position: relative; z-index: 100000; box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.18), 0 12px 30px rgba(31, 42, 61, 0.14); border-radius: 8px; } /* 深色主题适配 */ .guide-modal.dark-theme { --guide-bg: #111827; --guide-bg-subtle: #182235; --guide-bg-hover: #223047; --guide-border: #2c3a51; --guide-border-strong: #3e4f68; --guide-text: #e7eef9; --guide-text-muted: #a8b4c7; --guide-primary: #60a5fa; --guide-primary-hover: #93c5fd; --guide-primary-soft: rgba(96, 165, 250, 0.16); } .dark-theme .guide-content { background: var(--guide-bg); } .dark-theme .guide-title { color: var(--guide-text); } .dark-theme .guide-text { color: var(--guide-text-muted); } .dark-theme .guide-footer { background: var(--guide-bg-subtle); border-top-color: var(--guide-border); } .dark-theme .guide-footer .guide-prev { background: var(--guide-bg) !important; border-color: var(--guide-border) !important; color: var(--guide-text-muted) !important; } .dark-theme .guide-footer .guide-prev:disabled { background: #111827 !important; border-color: #1f2937 !important; color: #64748b !important; } .dark-theme .guide-dot { background: var(--guide-border-strong); } .dark-theme .guide-dot.active { background: var(--guide-primary); } `; } const CONFIG_VERSION = "1.0"; function exportConfig() { const aiConfigs = {}; Object.keys(AI_PRESETS).forEach((aiType) => { aiConfigs[aiType] = ConfigManager.getAIConfig(aiType); }); const exportData = { version: CONFIG_VERSION, exportedAt: Date.now(), learning: { ...CONFIG.learning }, exam: { ...CONFIG.exam }, theme: CONFIG.theme, aiConfigs }; return JSON.stringify(exportData, null, 2); } function downloadConfig() { const configJson = exportConfig(); const blob = new Blob([configJson], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `icve-helper-config-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Logger.success("配置已导出"); } function importConfig(jsonString) { try { const data = JSON.parse(jsonString); if (!data.version) { return { success: false, message: "无效的配置文件格式" }; } if (data.learning) { Object.assign(CONFIG.learning, data.learning); } if (data.exam) { Object.assign(CONFIG.exam, data.exam); } if (data.theme) { CONFIG.theme = data.theme; } if (data.aiConfigs) { Object.entries(data.aiConfigs).forEach(([aiType, config]) => { if (config.apiKey) { GM_setValue(`ai_key_${aiType}`, config.apiKey); } if (config.baseURL) { GM_setValue(`ai_baseurl_${aiType}`, config.baseURL); } if (config.model) { GM_setValue(`ai_model_${aiType}`, config.model); } }); } saveConfig(); Logger.success("配置导入成功"); return { success: true, message: "配置导入成功,页面将刷新以应用更改" }; } catch (error) { Logger.error("配置导入失败:", error); return { success: false, message: "配置文件解析失败,请检查文件格式" }; } } function resetToDefault() { CONFIG.learning = { ...ConfigManager.defaults.learning }; CONFIG.exam = { ...ConfigManager.defaults.exam }; CONFIG.theme = "light"; saveConfig(); Object.keys(AI_PRESETS).forEach((aiType) => { GM_setValue(`ai_key_${aiType}`, ""); GM_setValue(`ai_baseurl_${aiType}`, AI_PRESETS[aiType].baseURL); GM_setValue(`ai_model_${aiType}`, AI_PRESETS[aiType].model); }); Logger.warn("配置已重置为默认值"); } function createFileInput(onImport) { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.style.display = "none"; input.addEventListener("change", (e) => { var _a; const file = (_a = e.target.files) == null ? void 0 : _a[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { var _a2; const content = (_a2 = event.target) == null ? void 0 : _a2.result; const result = importConfig(content); onImport(result); }; reader.readAsText(file); }); return input; } function showConfirmDialog(options) { return new Promise((resolve) => { var _a, _b; const dialog = document.createElement("div"); dialog.className = "confirm-dialog-overlay"; dialog.innerHTML = `

${options.title}

${options.message}

`; const close = (result) => { dialog.classList.add("closing"); setTimeout(() => { dialog.remove(); resolve(result); }, 200); }; (_a = dialog.querySelector(".confirm-dialog-cancel")) == null ? void 0 : _a.addEventListener("click", () => close(false)); (_b = dialog.querySelector(".confirm-dialog-confirm")) == null ? void 0 : _b.addEventListener("click", () => close(true)); dialog.addEventListener("click", (e) => { if (e.target === dialog) close(false); }); const handleEsc = (e) => { if (e.key === "Escape") { close(false); document.removeEventListener("keydown", handleEsc); } }; document.addEventListener("keydown", handleEsc); document.body.appendChild(dialog); requestAnimationFrame(() => dialog.classList.add("visible")); }); } function showToast(message, type = "info", duration = 3e3) { const toast = document.createElement("div"); toast.className = `toast toast-${type}`; const icons = { success: "✅", error: "❌", info: "ℹ️", warning: "⚠️" }; toast.innerHTML = ` ${icons[type]} ${message} `; let container = document.getElementById("toast-container"); if (!container) { container = document.createElement("div"); container.id = "toast-container"; document.body.appendChild(container); } container.appendChild(toast); requestAnimationFrame(() => toast.classList.add("visible")); setTimeout(() => { toast.classList.remove("visible"); setTimeout(() => toast.remove(), 300); }, duration); } function getUIUtilsStyles() { return ` /* 按钮加载状态 */ .btn-loading { position: relative; pointer-events: none; } .btn-spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: btnSpin 0.6s linear infinite; vertical-align: middle; margin-right: 4px; } @keyframes btnSpin { to { transform: rotate(360deg); } } /* 确认对话框 */ .confirm-dialog-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 100002; opacity: 0; transition: opacity 0.2s; } .confirm-dialog-overlay.visible { opacity: 1; } .confirm-dialog-overlay.closing { opacity: 0; } .confirm-dialog { background: white; border-radius: 8px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); width: 90%; max-width: 360px; transform: scale(0.95); transition: transform 0.2s; } .confirm-dialog-overlay.visible .confirm-dialog { transform: scale(1); } .confirm-dialog-header { padding: 16px; border-bottom: 1px solid #e5e7eb; } .confirm-dialog-title { margin: 0; font-size: 16px; font-weight: 600; color: #1f2937; } .confirm-dialog-body { padding: 16px; } .confirm-dialog-message { margin: 0; font-size: 14px; color: #4b5563; line-height: 1.5; } .confirm-dialog-footer { padding: 12px 16px; background: #f9fafb; border-top: 1px solid #e5e7eb; border-radius: 0 0 8px 8px; display: flex; justify-content: flex-end; gap: 8px; } .confirm-dialog .btn { min-height: 34px; padding: 0 14px; border-radius: 9px; border: 1px solid transparent; cursor: pointer; font: inherit; font-size: 13px; font-weight: 700; transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease; } .confirm-dialog .btn-outline { background: #ffffff; color: #374151; border-color: #d1d5db; } .confirm-dialog .btn-outline:hover { background: #f3f4f6; border-color: #9ca3af; } .confirm-dialog .btn-primary { background: #3b82f6; color: #ffffff; border-color: #3b82f6; } .confirm-dialog .btn-primary:hover { background: #2563eb; border-color: #2563eb; } .confirm-dialog .btn-danger { background: #ef4444; color: #ffffff; border-color: #ef4444; } .confirm-dialog .btn-danger:hover { background: #dc2626; border-color: #dc2626; } /* Toast 提示 */ #toast-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 100003; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } .toast { background: white; padding: 10px 16px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); display: flex; align-items: center; gap: 8px; opacity: 0; transform: translateY(-10px); transition: all 0.3s ease; pointer-events: auto; } .toast.visible { opacity: 1; transform: translateY(0); } .toast-icon { font-size: 16px; } .toast-message { font-size: 13px; color: #1f2937; } .toast-success { border-left: 3px solid #10b981; } .toast-error { border-left: 3px solid #ef4444; } .toast-info { border-left: 3px solid #3b82f6; } .toast-warning { border-left: 3px solid #f59e0b; } /* 深色主题适配 */ .dark-theme .confirm-dialog { background: #1e293b; } .dark-theme .confirm-dialog-header { border-bottom-color: #334155; } .dark-theme .confirm-dialog-title { color: #f1f5f9; } .dark-theme .confirm-dialog-message { color: #94a3b8; } .dark-theme .confirm-dialog-footer { background: #0f172a; border-top-color: #334155; } .dark-theme .confirm-dialog .btn-outline { background: #111827; color: #e5e7eb; border-color: #374151; } .dark-theme .confirm-dialog .btn-outline:hover { background: #1f2937; border-color: #4b5563; } .dark-theme .toast { background: #1e293b; } .dark-theme .toast-message { color: #f1f5f9; } `; } const DOMCache = { _cache: /* @__PURE__ */ new Map(), _idCache: /* @__PURE__ */ new Map(), _maxAge: 5e3, _debug: false, /** * 通过选择器获取元素(带缓存) */ get(selector, forceRefresh = false) { const now = Date.now(); const cached = this._cache.get(selector); if (!forceRefresh && cached && now - cached.time < this._maxAge) { if (this._debug) { console.log(`[DOMCache] Hit: ${selector}`); } if (cached.element && document.contains(cached.element)) { return cached.element; } } const element = document.querySelector(selector); this._cache.set(selector, { element, time: now }); if (this._debug) { console.log(`[DOMCache] Miss: ${selector}`, element ? "found" : "not found"); } return element; }, /** * 通过 ID 获取元素(永久缓存) */ getById(id, forceRefresh = false) { if (!forceRefresh && this._idCache.has(id)) { const cached = this._idCache.get(id); if (document.contains(cached)) { return cached; } } const element = document.getElementById(id); if (element) { this._idCache.set(id, element); } else { this._idCache.delete(id); } return element; }, /** * 安全设置元素文本内容 */ setText(id, text) { const element = this.getById(id); if (element) { element.textContent = text; return true; } return false; }, /** * 安全设置元素样式 */ setStyle(id, styles) { const element = this.getById(id); if (element) { Object.assign(element.style, styles); return true; } return false; }, /** * 清除过期缓存 */ cleanup() { const now = Date.now(); for (const [selector, cached] of this._cache.entries()) { if (now - cached.time >= this._maxAge) { this._cache.delete(selector); } } } }; setInterval(() => { DOMCache.cleanup(); }, 3e4); var ErrorType = /* @__PURE__ */ ((ErrorType2) => { ErrorType2["NETWORK"] = "NETWORK"; ErrorType2["API"] = "API"; ErrorType2["PARSE"] = "PARSE"; ErrorType2["DOM"] = "DOM"; ErrorType2["CONFIG"] = "CONFIG"; ErrorType2["TIMEOUT"] = "TIMEOUT"; ErrorType2["VALIDATION"] = "VALIDATION"; ErrorType2["UNKNOWN"] = "UNKNOWN"; return ErrorType2; })(ErrorType || {}); class AppError extends Error { constructor(message, type = ErrorType.UNKNOWN, cause = null, context = {}) { super(message); this.name = "AppError"; this.type = type; this.cause = cause; this.context = context; this.timestamp = (/* @__PURE__ */ new Date()).toISOString(); } /** * 获取用户友好的错误消息 */ getUserMessage() { const messages = { [ErrorType.NETWORK]: "网络连接失败,请检查网络后重试", [ErrorType.API]: "API 请求失败,请检查配置", [ErrorType.PARSE]: "数据解析失败", [ErrorType.DOM]: "页面元素加载异常,请刷新页面", [ErrorType.CONFIG]: "配置错误,请检查设置", [ErrorType.TIMEOUT]: "操作超时,请重试", [ErrorType.VALIDATION]: "输入验证失败", [ErrorType.UNKNOWN]: "发生未知错误" }; return messages[this.type] || this.message; } } const ErrorHandler = { /** * 处理错误并记录日志 */ handle(error, context = "", silent = false) { const appError = this.normalize(error, context); Logger.error(`[${appError.type}] ${context ? context + ": " : ""}${appError.message}`); if (this._isDebugMode()) { console.error("[ICVE Helper Error]", { type: appError.type, message: appError.message, context: appError.context, cause: appError.cause, stack: appError.stack }); } if (!silent) { this._showUserNotification(appError); } return appError; }, /** * 将普通错误转换为 AppError */ normalize(error, context = "") { if (error instanceof AppError) { return error; } let type = ErrorType.UNKNOWN; let message = error.message || "未知错误"; if (error.name === "TypeError" && message.includes("fetch")) { type = ErrorType.NETWORK; message = "网络请求失败"; } else if (error.name === "SyntaxError" || message.includes("JSON")) { type = ErrorType.PARSE; message = "JSON 解析失败"; } else if (message.includes("timeout") || message.includes("超时")) { type = ErrorType.TIMEOUT; } else if (message.includes("API") || message.includes("401") || message.includes("403")) { type = ErrorType.API; } else if (error.name === "TypeError" && (message.includes("null") || message.includes("undefined"))) { type = ErrorType.DOM; } return new AppError(message, type, error, { originalContext: context }); }, /** * 创建 API 错误 */ createAPIError(status, message = "") { const statusMessages = { 400: "请求参数错误", 401: "API Key 无效或已过期", 403: "没有访问权限", 404: "API 地址不存在", 429: "请求过于频繁,请稍后重试", 500: "API 服务器内部错误", 502: "API 网关错误", 503: "API 服务暂时不可用" }; const errorMessage = message || statusMessages[status] || `API 错误 (${status})`; return new AppError(errorMessage, ErrorType.API, null, { status }); }, /** * 创建网络错误 */ createNetworkError(message = "网络连接失败") { return new AppError(message, ErrorType.NETWORK); }, /** * 创建超时错误 */ createTimeoutError(timeout) { const message = timeout ? `请求超时(${timeout / 1e3}秒)` : "请求超时"; return new AppError(message, ErrorType.TIMEOUT, null, { timeout }); }, /** * 安全执行函数,捕获错误 */ safeExecute(fn, defaultValue = null, context = "") { try { return fn(); } catch (error) { this.handle(error, context, true); return defaultValue; } }, /** * 安全执行异步函数 */ async safeExecuteAsync(fn, defaultValue = null, context = "") { try { return await fn(); } catch (error) { this.handle(error, context, true); return defaultValue; } }, /** * 检查是否是调试模式 */ _isDebugMode() { return localStorage.getItem("icve_debug_mode") === "true"; }, /** * 显示用户通知 */ _showUserNotification(error) { const examMessage = document.getElementById("exam-message"); const learningProgressText = document.getElementById("learning-progress-text"); const userMessage = `❌ ${error.getUserMessage()}`; if (examMessage) { examMessage.textContent = userMessage; examMessage.style.color = "#ef4444"; } if (learningProgressText) { learningProgressText.textContent = userMessage; } } }; const MEDIA_PROGRESS_THROTTLE = 500; function isExamNode(nodeElement) { var _a; const examButton = nodeElement.querySelector(".li_action .btn_dt"); if (examButton) { const btnText = ((_a = examButton.textContent) == null ? void 0 : _a.trim()) || ""; if (btnText.includes("开始答题") || btnText.includes("答题") || btnText.includes("考试") || btnText.includes("测验")) { return true; } } return false; } function scanLearningNodes() { const nodes = document.querySelectorAll(".panelList .node"); state.learning.allNodes = []; state.learning.completedCount = 0; state.learning.examCount = 0; state.learning.totalCount = nodes.length; nodes.forEach((node, index) => { var _a; const nodeElement = node; const titleElement = nodeElement.querySelector(".title"); const statusIcon = nodeElement.querySelector(".jd"); const title = titleElement ? ((_a = titleElement.textContent) == null ? void 0 : _a.trim()) || `节点${index + 1}` : `节点${index + 1}`; const id = nodeElement.id; const isCompleted = statusIcon && statusIcon.classList.contains("wc") || state.learning.processedNodes.has(id); const isExam = isExamNode(nodeElement); if (isExam) { state.learning.examCount++; } state.learning.allNodes.push({ element: nodeElement, id, title, isCompleted, isExam, index }); if (isCompleted) { state.learning.completedCount++; } }); const uncompletedCount = state.learning.totalCount - state.learning.completedCount; Logger.info(`扫描完成: 共${state.learning.totalCount}个节点, 已完成${state.learning.completedCount}个, 待学习${uncompletedCount}个`); if (state.learning.examCount > 0) { Logger.info(`发现${state.learning.examCount}个考试节点(将自动跳过)`); } updateLearningStatus(); } function updateLearningStatus() { const progressText = `${state.learning.completedCount}/${state.learning.totalCount}`; const progressElement = DOMCache.getById("learning-progress"); if (progressElement) { progressElement.textContent = progressText; progressElement.title = state.learning.examCount > 0 ? `跳过 ${state.learning.examCount} 个考试/测验节点` : ""; } DOMCache.setText("learning-processed", String(state.learning.processedNodes.size)); const currentElement = DOMCache.getById("learning-current"); if (currentElement) { if (state.learning.currentNode && state.learning.currentNode.title) { const shortTitle = state.learning.currentNode.title.length > 18 ? state.learning.currentNode.title.substring(0, 18) + "..." : state.learning.currentNode.title; currentElement.textContent = shortTitle; currentElement.title = state.learning.currentNode.title; } else { currentElement.textContent = "无"; currentElement.title = ""; } } } function applyPlaybackRate() { const mediaElements = [ ...Array.from(document.querySelectorAll("audio")), ...Array.from(document.querySelectorAll("video")) ]; mediaElements.forEach((media) => { media.playbackRate = CONFIG.learning.playbackRate; }); } function applyMuteToCurrentMedia() { const mediaElements = [ ...Array.from(document.querySelectorAll("audio")), ...Array.from(document.querySelectorAll("video")) ]; mediaElements.forEach((media) => { media.muted = CONFIG.learning.muteMedia; }); } function resetLearning() { state.learning.processedNodes.clear(); if (state.learning.completedChapters) { state.learning.completedChapters.clear(); } saveLearningProgress(); scanLearningNodes(); Logger.warn("已重置所有学习进度"); } function updateLearningProgressText(text) { const progressText = document.getElementById("learning-progress-text"); if (progressText) { progressText.textContent = text; } } async function fetchChapterContentByAPI(chapterId) { try { const urlParams = new URLSearchParams(window.location.search); const courseInfoId = urlParams.get("courseInfoId"); const courseId = urlParams.get("courseId"); if (!courseInfoId || !courseId) { Logger.warn("无法获取课程参数,跳过 API 预加载"); return null; } const apiUrl = `https://ai.icve.com.cn/prod-api/course/courseDesign/getCellList?courseInfoId=${courseInfoId}&courseId=${courseId}&parentId=${chapterId}`; const response = await fetch(apiUrl, { method: "GET", headers: { "Accept": "application/json, text/plain, */*", "Content-Type": "application/json" }, credentials: "include" }); if (!response.ok) { Logger.warn(`章节内容 API 返回 ${response.status},将使用点击方式展开`); return null; } const data = await response.json(); return data; } catch (error) { ErrorHandler.handle(error, "获取章节内容", true); return null; } } async function expandNextUncompletedSection() { var _a; updateLearningProgressText("🔍 正在查找下一个章节..."); const sections = document.querySelectorAll(".one > .draggablebox > span > .collapse-panel"); for (const section of Array.from(sections)) { const sectionElement = section; const panelTitle = sectionElement.querySelector(".panel-title"); const panelContent = sectionElement.querySelector(".panel-content"); if (!panelTitle || !panelContent) continue; if (panelContent.style.display !== "none") { const nodes = sectionElement.querySelectorAll(".panelList .node"); if (nodes.length > 0) { const allCompleted = Array.from(nodes).every((node) => { const nodeElement = node; const statusIcon = nodeElement.querySelector(".jd"); const id = nodeElement.id; const isExam = isExamNode(nodeElement); return statusIcon && statusIcon.classList.contains("wc") || state.learning.processedNodes.has(id) || isExam; }); if (allCompleted) { const chapterId = sectionElement.id; if (!state.learning.completedChapters) { state.learning.completedChapters = /* @__PURE__ */ new Set(); } state.learning.completedChapters.add(chapterId); saveLearningProgress(); continue; } else { return false; } } } else { const chapterId = sectionElement.id; if (state.learning.completedChapters && state.learning.completedChapters.has(chapterId)) { continue; } const titleText = (((_a = panelTitle.textContent) == null ? void 0 : _a.trim()) || "").substring(0, 40); updateLearningProgressText(`📂 正在展开新章节:${titleText}...`); await fetchChapterContentByAPI(chapterId); await Utils.sleep(500); const arrow = panelTitle.querySelector(".jiantou"); if (arrow) { arrow.click(); await Utils.sleep(800); } await Utils.sleep(2e3); let nodes = sectionElement.querySelectorAll(".panelList .node"); let retryCount = 0; const maxRetries = 5; while (nodes.length === 0 && retryCount < maxRetries) { await Utils.sleep(1500); nodes = sectionElement.querySelectorAll(".panelList .node"); retryCount++; if (nodes.length === 0 && retryCount === 2) { const retryArrow = panelTitle.querySelector(".jiantou"); if (retryArrow) { retryArrow.click(); await Utils.sleep(1e3); } } } updateLearningProgressText(`✅ 章节展开成功,发现 ${nodes.length} 个节点`); Logger.info(`展开新章节: 发现${nodes.length}个节点`); if (nodes.length > 0) { return true; } else { if (!state.learning.completedChapters) { state.learning.completedChapters = /* @__PURE__ */ new Set(); } state.learning.completedChapters.add(chapterId); saveLearningProgress(); continue; } } } return false; } function getDocumentPageInfo() { var _a; const pageDiv = document.querySelector(".page"); if (!pageDiv) return null; const match = (_a = pageDiv.textContent) == null ? void 0 : _a.match(/(\d+)\s*\/\s*(\d+)/); if (match) { return { current: parseInt(match[1]), total: parseInt(match[2]) }; } return null; } function clickNextPage() { var _a; const buttons = document.querySelectorAll(".page button"); for (const btn of Array.from(buttons)) { const span = btn.querySelector("span"); if (span && ((_a = span.textContent) == null ? void 0 : _a.includes("下一页"))) { btn.click(); return true; } } return false; } function handleDocument() { const pageInfo = getDocumentPageInfo(); if (pageInfo) { state.learning.currentPage = pageInfo.current; state.learning.totalPages = pageInfo.total; const percentage = pageInfo.current / pageInfo.total * 100; Utils.updateProgressBar("learning-progress-bar", percentage); updateLearningProgressText(`文档: 第 ${pageInfo.current}/${pageInfo.total} 页`); if (pageInfo.current < pageInfo.total) { setTimeout(() => { if (clickNextPage()) { setTimeout(() => { handleDocument(); }, 2e3); } }, CONFIG.learning.documentPageInterval * 1e3); } else { updateLearningProgressText("文档已浏览完成"); Logger.success(`文档浏览完成(共${pageInfo.total}页)`); state.learning.isDocument = false; setTimeout(() => { Utils.resetProgressBar("learning-progress-bar"); }, 1e3); if (state.learning.currentNode && state.learning.currentNode.id) { state.learning.processedNodes.add(state.learning.currentNode.id); saveLearningProgress(); updateLearningStatus(); } if (state.learning.isRunning) { setTimeout(() => { goToNextNode(); }, CONFIG.learning.waitTimeAfterComplete * 1e3); } } } else { updateLearningProgressText("单页文档已浏览"); Logger.success("单页文档浏览完成"); state.learning.isDocument = false; if (state.learning.currentNode && state.learning.currentNode.id) { state.learning.processedNodes.add(state.learning.currentNode.id); saveLearningProgress(); updateLearningStatus(); } if (state.learning.isRunning) { setTimeout(() => { goToNextNode(); }, CONFIG.learning.waitTimeAfterComplete * 1e3); } } } function hideContinuePlayDialog() { const dialogs = document.querySelectorAll(".el-message-box__wrapper"); for (const dialog of Array.from(dialogs)) { const dialogElement = dialog; if (dialogElement.style.display === "none") continue; const dialogText = (dialogElement.textContent || "").replace(/\s+/g, " "); if (dialogText.includes("继续播放") || dialogText.includes("是否继续") && dialogText.includes("播放")) { dialogElement.style.display = "none"; Logger.info('已隐藏"继续播放"提示框'); return true; } } return false; } function playMedia(mediaElements) { mediaElements.forEach((media) => { if (media.dataset.processed) return; media.dataset.processed = "true"; const mediaType = media.tagName.toLowerCase() === "video" ? "视频" : "音频"; media.playbackRate = CONFIG.learning.playbackRate; media.muted = CONFIG.learning.muteMedia; updateLearningProgressText(`${mediaType}播放中...`); let lastUpdateTime = 0; const throttledProgressUpdate = () => { const now = Date.now(); if (now - lastUpdateTime < MEDIA_PROGRESS_THROTTLE) return; lastUpdateTime = now; if (media.duration > 0) { const current = media.currentTime; const total = media.duration; const percentage = current / total * 100; Utils.updateProgressBar("learning-progress-bar", percentage); updateLearningProgressText(`${mediaType}: ${Utils.formatTime(current)} / ${Utils.formatTime(total)}`); } }; media.addEventListener("timeupdate", throttledProgressUpdate); media.addEventListener("ended", () => { state.learning.mediaWatching = false; Logger.success(`${mediaType}播放完成`); if (state.learning.currentNode && state.learning.currentNode.id) { state.learning.processedNodes.add(state.learning.currentNode.id); saveLearningProgress(); updateLearningStatus(); } Utils.resetProgressBar("learning-progress-bar"); updateLearningProgressText(`${mediaType}已完成`); if (state.learning.isRunning) { setTimeout(() => { goToNextNode(); }, CONFIG.learning.waitTimeAfterComplete * 1e3); } }); state.learning.mediaWatching = true; media.play().catch((err) => { state.learning.mediaWatching = false; Logger.error("媒体播放失败: " + err.message); }); }); } function detectContentType() { var _a; const examButton = document.querySelector(".li_action .btn_dt, .btn_dt"); if (examButton) { const btnText = ((_a = examButton.textContent) == null ? void 0 : _a.trim()) || ""; if (btnText.includes("开始答题") || btnText.includes("答题") || btnText.includes("考试") || btnText.includes("测验")) { updateLearningProgressText("⏭️ 检测到考试页面,已跳过"); Logger.warn("跳过考试节点"); if (state.learning.currentNode && state.learning.currentNode.id) { state.learning.processedNodes.add(state.learning.currentNode.id); saveLearningProgress(); updateLearningStatus(); } if (state.learning.isRunning) { setTimeout(() => { goToNextNode(); }, 1e3); } return; } } const mediaElements = [ ...Array.from(document.querySelectorAll("audio")), ...Array.from(document.querySelectorAll("video")) ]; if (mediaElements.length === 0) { updateLearningProgressText("检测到文档,准备浏览..."); Logger.info("检测到文档类型内容"); state.learning.isDocument = true; setTimeout(() => { handleDocument(); }, 1e3); return; } state.learning.isDocument = false; const mediaType = mediaElements[0].tagName.toLowerCase() === "video" ? "视频" : "音频"; Logger.info(`检测到${mediaType}内容,开始播放`); playMedia(mediaElements); } function safeClick(element) { try { const clickEvent = new MouseEvent("click", { bubbles: true, cancelable: true, view: window }); element.dispatchEvent(clickEvent); return true; } catch { try { element.click(); return true; } catch { return false; } } } async function clickNode(nodeInfo) { state.learning.currentNode = nodeInfo; updateLearningStatus(); Utils.resetProgressBar("learning-progress-bar"); updateLearningProgressText("正在加载内容..."); const shortTitle = nodeInfo.title.length > 25 ? nodeInfo.title.substring(0, 25) + "..." : nodeInfo.title; Logger.info(`开始学习: ${shortTitle}`); let targetElement = null; if (nodeInfo.id) { targetElement = document.getElementById(nodeInfo.id); } if (!targetElement && nodeInfo.element) { try { if (nodeInfo.element.isConnected) { targetElement = nodeInfo.element; } } catch { } } if (targetElement) { if (safeClick(targetElement)) { setTimeout(() => { detectContentType(); }, 3e3); return; } } Logger.warn("无法点击节点,重新扫描"); scanLearningNodes(); setTimeout(() => { goToNextNode(); }, 1e3); } async function goToNextNode() { scanLearningNodes(); const uncompletedNodes = state.learning.allNodes.filter((n) => !n.isCompleted); if (uncompletedNodes.length === 0) { updateLearningProgressText("🎯 当前章节已完成,正在查找下一章节..."); const foundNewSection = await expandNextUncompletedSection(); if (foundNewSection) { scanLearningNodes(); const newUncompletedNodes = state.learning.allNodes.filter((n) => !n.isCompleted); if (newUncompletedNodes.length > 0) { const nextNode2 = newUncompletedNodes[0]; if (nextNode2.isExam) { updateLearningProgressText(`⏭️ 跳过考试节点:${nextNode2.title.substring(0, 20)}...`); state.learning.processedNodes.add(nextNode2.id); saveLearningProgress(); updateLearningStatus(); setTimeout(() => { goToNextNode(); }, 500); return; } setTimeout(() => { clickNode(nextNode2); }, 1e3); } else { setTimeout(() => { goToNextNode(); }, 1e3); } } else { updateLearningProgressText("🎉 所有章节已完成!"); Logger.success("所有学习内容已完成!"); state.learning.isRunning = false; const startBtn = document.getElementById("learning-start"); if (startBtn) startBtn.disabled = false; const statusEl = document.getElementById("learning-status"); if (statusEl) statusEl.textContent = "已完成"; const statusDot = document.getElementById("learning-status-dot"); if (statusDot) { statusDot.classList.remove("running"); statusDot.classList.add("completed"); } } return; } const nextNode = uncompletedNodes[0]; if (nextNode.isExam) { updateLearningProgressText(`⏭️ 跳过考试节点:${nextNode.title.substring(0, 20)}...`); state.learning.processedNodes.add(nextNode.id); saveLearningProgress(); updateLearningStatus(); setTimeout(() => { goToNextNode(); }, 500); return; } setTimeout(() => { clickNode(nextNode); }, 1e3); } function startLearning() { if (state.learning.isRunning) return; state.learning.isRunning = true; const startBtn = document.getElementById("learning-start"); if (startBtn) startBtn.disabled = true; const statusEl = document.getElementById("learning-status"); if (statusEl) statusEl.textContent = "运行中"; const statusDot = document.getElementById("learning-status-dot"); if (statusDot) statusDot.classList.add("running"); setTimeout(() => { hideContinuePlayDialog(); }, 500); Logger.info("开始自动学习"); scanLearningNodes(); setTimeout(() => { goToNextNode(); }, 1e3); } function getAIConfig() { return ConfigManager.getAIConfig(CONFIG.exam.currentAI); } function getCurrentQuestion() { var _a; const questionEl = document.querySelector(".single, .multiple, .judge, .fill, .completion"); if (!questionEl) return null; const typeMap = { "single": "单选题", "multiple": "多选题", "judge": "判断题", "fill": "填空题", "completion": "填空题" }; let questionType = "未知"; for (const [cls, type] of Object.entries(typeMap)) { if (questionEl.classList.contains(cls)) { questionType = type; break; } } const titleEl = questionEl.querySelector(".single-title-content, .multiple-title-content, .judge-title-content, .fill-title-content, .completion-title-content"); const questionText = titleEl ? ((_a = titleEl.textContent) == null ? void 0 : _a.trim()) || "" : ""; const options = []; const optionEls = questionEl.querySelectorAll(".ivu-radio-wrapper, .ivu-checkbox-wrapper"); optionEls.forEach((optionEl, index) => { var _a2; const optionLabel = String.fromCharCode(65 + index); const optionTextEl = optionEl.querySelector("span:last-child"); const optionText = optionTextEl ? ((_a2 = optionTextEl.textContent) == null ? void 0 : _a2.trim()) || "" : ""; options.push({ label: optionLabel, text: optionText, element: optionEl }); }); let fillInputs = []; if (questionType === "填空题") { fillInputs = Array.from(questionEl.querySelectorAll('input[type="text"], textarea, .ivu-input')); } return { type: questionType, text: questionText, options, fillInputs, element: questionEl }; } function buildPrompt(question) { let prompt = ""; if (question.type === "单选题") { prompt = `这是一道单选题,请仔细分析后选择正确答案。 题目:${question.text} 选项: `; question.options.forEach((opt) => { prompt += `${opt.label}. ${opt.text} `; }); prompt += ` 请直接回答选项字母(如:A 或 B 或 C 或 D),不要有其他内容。`; } else if (question.type === "多选题") { prompt = `这是一道多选题,请仔细分析后选择所有正确答案。 题目:${question.text} 选项: `; question.options.forEach((opt) => { prompt += `${opt.label}. ${opt.text} `; }); prompt += ` 请直接回答选项字母,多个答案用逗号分隔(如:A,C,D),不要有其他内容。`; } else if (question.type === "判断题") { prompt = `这是一道判断题,请判断对错。 题目:${question.text} `; if (question.options.length > 0) { prompt += `选项: `; question.options.forEach((opt) => { prompt += `${opt.label}. ${opt.text} `; }); prompt += ` 请直接回答选项字母(如:A 或 B),不要有其他内容。`; } else { prompt += ` 请直接回答"对"或"错",不要有其他内容。`; } } else if (question.type === "填空题") { prompt = `这是一道填空题,请给出准确答案。 题目:${question.text} `; if (question.options && question.options.length > 0) { prompt += `参考选项: `; question.options.forEach((opt) => { prompt += `${opt.label}. ${opt.text} `; }); prompt += ` `; } const blankCount = question.fillInputs.length; if (blankCount > 1) { prompt += `注意:这道题有 ${blankCount} 个空需要填写。 `; prompt += `请按顺序给出所有空的答案,每个答案之间用分号(;)分隔。 例如:答案1;答案2;答案3 `; } prompt += `要求: 1. 只返回答案内容,不要有任何解释或其他文字 2. 如果有多个空,务必用分号(;)分隔 3. 答案要准确简洁`; } return prompt; } function getChatCompletionsUrl(baseURL) { return `${baseURL.trim().replace(/\/+$/, "")}/chat/completions`; } function requestChatCompletion(requestBody, apiKey, timeoutMs = 3e4) { return new Promise((resolve, reject) => { const aiConfig = getAIConfig(); const timeoutId = setTimeout(() => { reject(new Error("请求超时,请检查网络连接")); }, timeoutMs); GM_xmlhttpRequest({ method: "POST", url: getChatCompletionsUrl(aiConfig.baseURL), headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, data: JSON.stringify(requestBody), timeout: timeoutMs, onload: function(response) { var _a, _b, _c; clearTimeout(timeoutId); try { if (response.status === 401) { reject(new Error("API_KEY_EXPIRED")); return; } if (response.status === 403) { reject(new Error("API Key 权限不足或账户余额不足")); return; } if (response.status === 429) { reject(new Error("请求频率过高,请稍后再试")); return; } if (response.status === 500 || response.status === 502 || response.status === 503) { reject(new Error("AI 服务暂时不可用,请稍后再试")); return; } if (response.status < 200 || response.status >= 300) { let errorMsg = `API 错误 (${response.status})`; try { const errorData = JSON.parse(response.responseText); if ((_a = errorData.error) == null ? void 0 : _a.message) { errorMsg = errorData.error.message; } } catch { } reject(new Error(errorMsg)); return; } const data = JSON.parse(response.responseText); if (data.choices && data.choices.length > 0 && ((_b = data.choices[0].message) == null ? void 0 : _b.content)) { resolve(data.choices[0].message.content.trim()); } else if (data.error) { reject(new Error(data.error.message || "API 返回错误")); } else { reject(new Error("AI 返回数据异常,请检查 API 配置")); } } catch (error) { Logger.error("解析响应失败:", (_c = response.responseText) == null ? void 0 : _c.substring(0, 200)); reject(new Error("解析 AI 响应失败,请检查 API 地址是否正确")); } }, onerror: (err) => { clearTimeout(timeoutId); Logger.error("网络错误:", err); reject(new Error("网络请求失败,请检查网络连接和 API 地址")); }, ontimeout: () => { clearTimeout(timeoutId); reject(new Error("请求超时,请检查网络连接")); } }); }); } function doAskAI(question, apiKey) { const aiConfig = getAIConfig(); const prompt = buildPrompt(question); return requestChatCompletion({ model: aiConfig.model, messages: [ { role: "system", content: "你是一个专业的答题助手。你需要根据题目内容,给出准确的答案。请严格按照要求的格式返回答案。" }, { role: "user", content: prompt } ], temperature: 0.1, max_tokens: 500 }, apiKey); } async function testAIConnection() { const aiConfig = getAIConfig(); if (!aiConfig.apiKey.trim()) { return { success: false, message: "请先填写 API Key" }; } if (!aiConfig.baseURL.trim()) { return { success: false, message: "请先填写 API 地址" }; } if (!aiConfig.model.trim()) { return { success: false, message: "请先填写模型名称" }; } try { await requestChatCompletion({ model: aiConfig.model, messages: [ { role: "system", content: "你是一个接口连通性测试助手。" }, { role: "user", content: "请只回复 OK" } ], temperature: 0, max_tokens: 8 }, aiConfig.apiKey, 2e4); const aiType = normalizeAIType(CONFIG.exam.currentAI); return { success: true, message: `${AI_PRESETS[aiType].name} 测试通过` }; } catch (error) { const err = error; if (err.message === "API_KEY_EXPIRED") { return { success: false, message: "API Key 无效或已过期" }; } return { success: false, message: err.message || "测试失败" }; } } async function askAI(question) { const aiConfig = getAIConfig(); Logger.info(`正在请求AI...`); try { return await doAskAI(question, aiConfig.apiKey); } catch (error) { const err = error; if (err.message === "API_KEY_EXPIRED") { throw new Error("API Key 无效或已过期,请检查配置"); } throw error; } } async function searchAnswer(question) { try { const aiConfig = getAIConfig(); if (!aiConfig.apiKey || aiConfig.apiKey === "") { updateExamMessage("请先配置API Key", "#ef4444"); return null; } const aiType = normalizeAIType(CONFIG.exam.currentAI); updateExamMessage(`📡 正在使用 ${AI_PRESETS[aiType].name} 查询...`, "#2196F3"); const answer = await Utils.retry( () => askAI(question), 2, // 最多重试2次 1500 // 重试间隔1.5秒 ); return answer; } catch (error) { Logger.error("查询失败:", error.message); updateExamMessage("❌ 查询失败: " + error.message, "#ef4444"); return null; } } async function selectAnswer(question, answer) { if (!answer) { updateExamMessage("未找到答案,跳过此题", "#f59e0b"); return false; } try { if (question.type === "单选题" || question.type === "判断题") { const matchedOption = question.options.find((opt) => { return answer.includes(opt.label) || answer.includes(opt.text) || opt.text.includes(answer); }); if (matchedOption) { const radioInput = matchedOption.element.querySelector('input[type="radio"]'); if (radioInput) { radioInput.click(); updateExamMessage(`已选择答案:${matchedOption.label}`, "#10b981"); return true; } } } else if (question.type === "多选题") { const answerLabels = answer.match(/[A-Z]/g) || []; let selectedCount = 0; for (let i = 0; i < answerLabels.length; i++) { const label = answerLabels[i]; const matchedOption = question.options.find((opt) => opt.label === label); if (matchedOption) { let checkboxInput = matchedOption.element.querySelector('input[type="checkbox"]'); if (!checkboxInput) { checkboxInput = matchedOption.element.querySelector(".ivu-checkbox-input"); } if (!checkboxInput) { matchedOption.element.click(); selectedCount++; } else if (!checkboxInput.checked) { checkboxInput.click(); selectedCount++; } await Utils.sleep(200); } } if (selectedCount > 0) { updateExamMessage(`已选择答案:${answerLabels.join(", ")}`, "#10b981"); return true; } } else if (question.type === "填空题") { if (question.fillInputs.length > 0) { const answers = answer.split(/[;;]/).map((a) => a.trim()).filter((a) => a); let filledCount = 0; question.fillInputs.forEach((input, index) => { if (answers[index]) { input.value = answers[index]; input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); input.dispatchEvent(new Event("blur", { bubbles: true })); filledCount++; } }); if (filledCount > 0) { updateExamMessage(`已填入 ${filledCount} 个答案`, "#10b981"); return true; } } } updateExamMessage("答案格式不匹配,跳过此题", "#f59e0b"); return false; } catch { return false; } } function clickNextButton() { const nextBtn = Array.from(document.querySelectorAll("button")).find((btn) => { var _a; return (_a = btn.textContent) == null ? void 0 : _a.includes("下一题"); }); if (nextBtn && !nextBtn.disabled) { setTimeout(() => { nextBtn.click(); updateExamMessage("已点击下一题", "#2196F3"); }, 500); return true; } return false; } async function clickSubmitButton() { const submitBtn = Array.from(document.querySelectorAll("button")).find((btn) => { var _a; return (_a = btn.textContent) == null ? void 0 : _a.includes("交卷"); }); if (submitBtn && !submitBtn.disabled) { if (CONFIG.exam.autoSubmit) { updateExamMessage("正在自动交卷...", "#10b981"); await Utils.sleep(1e3); submitBtn.click(); await Utils.sleep(1500); const confirmed = await clickConfirmSubmit(); if (confirmed) { updateExamMessage("已自动确认提交", "#10b981"); } } else { updateExamMessage("所有题目已完成,请手动交卷", "#10b981"); } return true; } return false; } async function clickConfirmSubmit() { for (let i = 0; i < 10; i++) { let confirmBtn = Array.from(document.querySelectorAll("button")).find((btn) => { var _a; return (_a = btn.textContent) == null ? void 0 : _a.includes("确认提交"); }); if (!confirmBtn) { const footer = document.querySelector(".ivu-modal-confirm-footer"); if (footer) confirmBtn = footer.querySelector(".ivu-btn-primary") ?? void 0; } if (!confirmBtn) { const modal = document.querySelector(".ivu-modal-confirm"); if (modal) confirmBtn = modal.querySelector(".ivu-btn-primary") ?? void 0; } if (confirmBtn) { await Utils.sleep(500); confirmBtn.click(); await Utils.sleep(2e3); await clickClosePage(); return true; } await Utils.sleep(100); } return false; } async function clickClosePage() { var _a, _b; for (let i = 0; i < 15; i++) { let closeBtn = Array.from(document.querySelectorAll("button")).find( (btn) => { var _a2, _b2; return ((_a2 = btn.textContent) == null ? void 0 : _a2.includes("关闭页面")) || ((_b2 = btn.textContent) == null ? void 0 : _b2.includes("关闭")); } ); if (!closeBtn) { const footer = document.querySelector(".ivu-modal-confirm-footer"); if (footer) { const primaryBtn = footer.querySelector(".ivu-btn-primary"); if (primaryBtn && (((_a = primaryBtn.textContent) == null ? void 0 : _a.includes("关闭")) || ((_b = primaryBtn.textContent) == null ? void 0 : _b.includes("确定")))) { closeBtn = primaryBtn; } } } if (closeBtn) { await Utils.sleep(500); closeBtn.click(); updateExamMessage("已完成并关闭页面", "#10b981"); return true; } await Utils.sleep(200); } return false; } async function answerQuestions() { while (state.exam.isRunning) { try { const question = getCurrentQuestion(); if (!question || !question.text) { const submitted = await clickSubmitButton(); if (submitted) break; await Utils.sleep(2e3); break; } state.exam.currentQuestionIndex++; updateExamProgress(); const shortQuestion = question.text.length > 50 ? question.text.substring(0, 50) + "..." : question.text; Logger.info(`【第${state.exam.currentQuestionIndex}题-${question.type}】${shortQuestion}`); if (question.options.length > 0) { const optionsText = question.options.map((opt) => `${opt.label}.${opt.text}`).join(" | "); const shortOptions = optionsText.length > 80 ? optionsText.substring(0, 80) + "..." : optionsText; Logger.info(`选项: ${shortOptions}`); } updateExamMessage(`正在处理第 ${state.exam.currentQuestionIndex} 题 (${question.type})...`, "#2196F3"); const answer = await searchAnswer(question); if (answer) { Logger.success(`AI答案: ${answer}`); const selected = await selectAnswer(question, answer); if (selected) { updateExamMessage(`✅ 第 ${state.exam.currentQuestionIndex} 题已完成`, "#10b981"); } else { Logger.error(`第${state.exam.currentQuestionIndex}题答案格式不匹配,已暂停`); updateExamMessage(`⚠️ 第 ${state.exam.currentQuestionIndex} 题答案格式不匹配,已暂停`, "#ef4444"); stopExam(); return; } } else { Logger.error(`第${state.exam.currentQuestionIndex}题未获取到答案,已暂停`); updateExamMessage(`❌ 第 ${state.exam.currentQuestionIndex} 题查询失败,已暂停`, "#ef4444"); stopExam(); return; } await Utils.sleep(CONFIG.exam.delay); const hasNext = clickNextButton(); if (!hasNext) { await Utils.sleep(1e3); await clickSubmitButton(); break; } await Utils.sleep(1e3); } catch (error) { Logger.error("答题出错:", error); updateExamMessage(`❌ 第 ${state.exam.currentQuestionIndex} 题出错: ${error.message},已暂停`, "#ef4444"); stopExam(); return; } } state.exam.isRunning = false; const startBtn = document.getElementById("exam-start"); const stopBtn = document.getElementById("exam-stop"); if (startBtn) startBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; const statusText = document.getElementById("exam-status"); const statusDot = document.getElementById("exam-status-dot"); if (statusText) statusText.textContent = "已完成"; if (statusDot) { statusDot.className = "status-dot completed"; } Logger.info("答题完成"); } async function startExam() { if (state.exam.isRunning) return; const aiConfig = getAIConfig(); if (!aiConfig.apiKey || aiConfig.apiKey === "") { updateExamMessage("❌ 请先配置 API Key", "#ef4444"); return; } state.exam.isRunning = true; state.exam.currentQuestionIndex = 0; state.exam.totalQuestions = getTotalQuestions(); const startBtn = document.getElementById("exam-start"); const stopBtn = document.getElementById("exam-stop"); if (startBtn) startBtn.disabled = true; if (stopBtn) stopBtn.disabled = false; const statusText = document.getElementById("exam-status"); const statusDot = document.getElementById("exam-status-dot"); if (statusText) statusText.textContent = "运行中"; if (statusDot) { statusDot.className = "status-dot running"; } const aiType = normalizeAIType(CONFIG.exam.currentAI); updateExamMessage(`开始AI答题(使用 ${AI_PRESETS[aiType].name})...`, "#10b981"); updateExamProgress(); await answerQuestions(); } function stopExam() { state.exam.isRunning = false; const startBtn = document.getElementById("exam-start"); const stopBtn = document.getElementById("exam-stop"); if (startBtn) startBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; const statusText = document.getElementById("exam-status"); const statusDot = document.getElementById("exam-status-dot"); if (statusText) statusText.textContent = "已停止"; if (statusDot) { statusDot.className = "status-dot"; } updateExamMessage("已停止答题", "#f59e0b"); } function getTotalQuestions() { const answerCard = DOMCache.get(".topic-zpx-list"); if (answerCard) { const questionSpans = answerCard.querySelectorAll(".topic-zpx-main span"); return questionSpans.length; } return 0; } function updateExamProgress() { DOMCache.setText("exam-progress", `${state.exam.currentQuestionIndex}/${state.exam.totalQuestions}`); const percentage = state.exam.totalQuestions > 0 ? state.exam.currentQuestionIndex / state.exam.totalQuestions * 100 : 0; Utils.updateProgressBar("exam-progress-bar", percentage); } function updateExamMessage(text, color = "#64748b") { DOMCache.setText("exam-message", text); DOMCache.setStyle("exam-message", { color }); } let suppressLauncherOpen = false; function getPageType() { const url = window.location.href; if (url.includes("/excellent-study/")) { return "learning"; } else if (url.includes("/preview-exam/")) { return "exam"; } return "all"; } function getDefaultTask(pageType) { return pageType === "exam" ? "exam" : "learning"; } function createTaskSwitcher(pageType, activeTask) { const tasks = []; if (pageType === "learning" || pageType === "all") { tasks.push({ key: "learning", label: "学习" }); } if (pageType === "exam" || pageType === "all") { tasks.push({ key: "exam", label: "答题" }); } tasks.push({ key: "config", label: "配置" }); const buttons = tasks.map((task) => ` `).join(""); return `
${buttons}
`; } function createLauncher(defaultTask) { const taskText = defaultTask === "exam" ? "答题就绪" : "学习就绪"; return ` `; } function createLearningWorkbench() { return `
当前任务

学习模式

停止中
进度 0/0
已处理 0
当前 等待开始
等待开始
倍速${CONFIG.learning.playbackRate}x
等待${CONFIG.learning.waitTimeAfterComplete} 秒
静音${CONFIG.learning.muteMedia ? "开" : "关"}
`; } function createExamWorkbench() { const aiType = normalizeAIType(CONFIG.exam.currentAI); const aiConfig = getAIConfig(); const preset = AI_PRESETS[aiType]; return `
当前任务

答题模式

就绪
进度 0/0
AI ${preset.name}
模型 ${aiConfig.model || "未配置模型"}
配置完成后点击开始
服务${preset.name}
延迟${CONFIG.exam.delay / 1e3} 秒
交卷${CONFIG.exam.autoSubmit ? "自动" : "手动"}
`; } function createConfigWorkbench() { const currentAI = normalizeAIType(CONFIG.exam.currentAI); const aiConfig = getAIConfig(); const preset = AI_PRESETS[currentAI]; const aiOptions = Object.entries(AI_PRESETS).map(([key, option]) => { const selected = currentAI === key ? "selected" : ""; return ``; }).join(""); return `

配置

${preset.name}
学习
答题
AI
未测试
管理
`; } function createPanel() { const panel = document.createElement("div"); panel.id = "icve-tabbed-panel"; const pageType = getPageType(); const defaultTask = getDefaultTask(pageType); const isOpen = localStorage.getItem("icve_workbench_open") === "true"; panel.className = isOpen ? "is-open" : "is-collapsed"; panel.dataset.task = defaultTask; panel.innerHTML = ` ${createLauncher(defaultTask)}
智慧职教助手
${createTaskSwitcher(pageType, defaultTask)}
${pageType === "learning" || pageType === "all" ? createLearningWorkbench() : ""} ${pageType === "exam" || pageType === "all" ? createExamWorkbench() : ""} ${createConfigWorkbench()}
日志
暂无事件
`; panel.querySelectorAll("[data-task-panel]").forEach((taskPanel) => { taskPanel.hidden = taskPanel.dataset.taskPanel !== defaultTask; }); panel.querySelectorAll("[data-task-select]").forEach((button) => { button.classList.toggle("active", button.dataset.taskSelect === defaultTask); }); addStyles(); addExtraStyles(); document.body.appendChild(panel); bindEvents(); applyTheme(CONFIG.theme); loadLearningProgress(); updateRecentEvents(); updateLauncherStatus(); setTimeout(() => { showGuide(); }, 500); } function addExtraStyles() { const style = document.createElement("style"); style.textContent = getGuideStyles() + getUIUtilsStyles(); document.head.appendChild(style); } function bindEvents() { const panel = document.getElementById("icve-tabbed-panel"); if (!panel) return; makeDraggable(); panel.addEventListener("click", handlePanelClick); panel.addEventListener("change", Utils.throttle(handlePanelChange, 150)); Logger.info("事件绑定完成"); } function updateRecentEvents() { const list = document.getElementById("recent-events-list"); if (!list) return; const recentLogs = Logger._logs.slice(-8).reverse(); if (recentLogs.length === 0) { list.innerHTML = '
暂无事件
'; return; } list.innerHTML = recentLogs.map((log) => `
${log.time} ${log.message}
`).join(""); } async function handlePanelClick(e) { var _a; const target = e.target; const id = target.id || ((_a = target.closest("[id]")) == null ? void 0 : _a.id); if (id === "icve-launcher" && suppressLauncherOpen) { suppressLauncherOpen = false; e.preventDefault(); return; } const actionMap = { "icve-launcher": openWorkbench, "theme-toggle": toggleTheme, "panel-toggle": closeWorkbench, "learning-start": startLearning, "learning-scan": scanLearningNodes, "learning-reset": handleResetLearning, "exam-start": handleStartExam, "exam-stop": stopExam, "exam-test-ai": handleTestAIConfig, "exam-test-ai-config": handleTestAIConfig, "exam-toggle-api-key": toggleApiKeyVisibility, "clear-recent-events": Logger.clearPageLog.bind(Logger), "export-config": downloadConfig, "import-config": handleImportConfig, "reset-config": handleResetConfig, "show-guide": () => { resetGuide(); showGuide(); } }; if (id && actionMap[id]) { await actionMap[id](); return; } const taskBtn = target.closest("[data-task-select]"); if (taskBtn == null ? void 0 : taskBtn.dataset.taskSelect) { switchTask(taskBtn.dataset.taskSelect); return; } } async function handleStartExam() { saveCurrentAIInputs(); await startExam(); } async function handleTestAIConfig() { const testBtns = [ document.getElementById("exam-test-ai"), document.getElementById("exam-test-ai-config") ].filter(Boolean); const configTestBtn = document.getElementById("exam-test-ai-config"); const configStatus = document.getElementById("exam-test-ai-status"); saveCurrentAIInputs(); updateAIProfileSummary(); if (configStatus) { configStatus.textContent = "测试中"; configStatus.title = "正在测试 AI 配置"; configStatus.className = "config-test-status is-pending"; } testBtns.forEach((button) => { button.disabled = true; button.textContent = button === configTestBtn ? "测试中" : "测试中..."; }); updateExamMessage("正在测试 AI 配置...", "#2196F3"); const result = await testAIConnection(); if (result.success) { updateExamMessage(`✅ ${result.message}`, "#10b981"); Logger.success(`AI 配置测试通过: ${result.message}`); if (configStatus) { configStatus.textContent = "通过"; configStatus.title = result.message; configStatus.className = "config-test-status is-success"; } } else { updateExamMessage(`❌ ${result.message}`, "#ef4444"); Logger.error(`AI 配置测试失败: ${result.message}`); if (configStatus) { configStatus.textContent = result.message; configStatus.title = result.message; configStatus.className = "config-test-status is-error"; } } testBtns.forEach((button) => { button.disabled = false; button.textContent = button === configTestBtn ? "测试" : "测试 AI"; }); } async function handleResetLearning() { const confirmed = await showConfirmDialog({ title: "重置学习进度", message: "确定要清空所有已处理节点的记录吗?此操作不可恢复。", confirmText: "确认重置", cancelText: "取消", danger: true }); if (confirmed) { resetLearning(); showToast("学习进度已重置", "success"); } } function handleImportConfig() { const fileInput = createFileInput((result) => { if (result.success) { showToast(result.message, "success"); setTimeout(() => { window.location.reload(); }, 1500); } else { showToast(result.message, "error"); } }); fileInput.click(); } async function handleResetConfig() { const confirmed = await showConfirmDialog({ title: "恢复默认配置", message: "确定要将所有配置重置为默认值吗?包括AI密钥等配置都将被清除。", confirmText: "确认重置", cancelText: "取消", danger: true }); if (confirmed) { resetToDefault(); showToast("配置已重置,页面将刷新", "success"); setTimeout(() => { window.location.reload(); }, 1500); } } function handlePanelChange(e) { const target = e.target; const id = target.id; const value = target.type === "checkbox" ? target.checked : target.value; switch (id) { case "learning-playback-rate": CONFIG.learning.playbackRate = parseFloat(value); applyPlaybackRate(); saveConfig(); Logger.info(`播放倍速: ${CONFIG.learning.playbackRate}x`); refreshQuickSettings(); break; case "learning-wait-time": CONFIG.learning.waitTimeAfterComplete = parseInt(value); saveConfig(); Logger.info(`完成等待时间: ${value}秒`); refreshQuickSettings(); break; case "learning-doc-interval": CONFIG.learning.documentPageInterval = parseInt(value); saveConfig(); Logger.info(`文档翻页间隔: ${value}秒`); break; case "learning-expand-delay": CONFIG.learning.expandDelay = parseFloat(value); saveConfig(); Logger.info(`展开延迟: ${value}秒`); break; case "learning-mute-media": CONFIG.learning.muteMedia = value; applyMuteToCurrentMedia(); saveConfig(); const toggleIcon = document.querySelector(".btn-toggle-label .toggle-icon"); if (toggleIcon) { toggleIcon.textContent = value ? "🔇" : "🔊"; } Logger.info(`静音模式: ${value ? "开启" : "关闭"}`); refreshQuickSettings(); break; case "exam-ai-model": CONFIG.exam.currentAI = normalizeAIType(value); const preset = AI_PRESETS[CONFIG.exam.currentAI]; const aiConfig = getAIConfig(); const apiKeyInputs = document.querySelectorAll("#exam-api-key"); const apiUrlInputs = document.querySelectorAll("#exam-api-url"); const modelInputs = document.querySelectorAll("#exam-api-model-name"); apiKeyInputs.forEach((apiKeyInput) => { apiKeyInput.value = aiConfig.apiKey; apiKeyInput.placeholder = preset.keyPlaceholder; }); apiUrlInputs.forEach((apiUrlInput) => { apiUrlInput.value = aiConfig.baseURL; }); modelInputs.forEach((modelInput) => { modelInput.value = aiConfig.model; }); updateAIProfileSummary(); const examMessage = document.getElementById("exam-message"); if (examMessage) examMessage.textContent = '💡 配置完成后点击"开始"'; updateExamMessage(`已切换到 ${preset.name}`, "#10b981"); setTimeout(() => { updateExamMessage(`就绪(使用 ${preset.name})`, "#64748b"); }, 2e3); saveConfig(); Logger.info(`AI模型: ${preset.name}`); break; case "exam-api-key": GM_setValue(`ai_key_${normalizeAIType(CONFIG.exam.currentAI)}`, value.trim()); document.querySelectorAll("#exam-api-key").forEach((input) => { if (input !== target) input.value = value.trim(); }); updateExamMessage("API Key已保存", "#10b981"); setTimeout(() => { const aiType = normalizeAIType(CONFIG.exam.currentAI); updateExamMessage(`就绪(使用 ${AI_PRESETS[aiType].name})`, "#64748b"); }, 2e3); Logger.info("API Key已更新"); break; case "exam-api-url": GM_setValue(`ai_baseurl_${normalizeAIType(CONFIG.exam.currentAI)}`, value.trim()); document.querySelectorAll("#exam-api-url").forEach((input) => { if (input !== target) input.value = value.trim(); }); updateAIProfileSummary(); updateExamMessage("API地址已保存", "#10b981"); setTimeout(() => { const aiType = normalizeAIType(CONFIG.exam.currentAI); updateExamMessage(`就绪(使用 ${AI_PRESETS[aiType].name})`, "#64748b"); }, 2e3); Logger.info(`API地址已更新`); break; case "exam-api-model-name": GM_setValue(`ai_model_${normalizeAIType(CONFIG.exam.currentAI)}`, value.trim()); document.querySelectorAll("#exam-api-model-name").forEach((input) => { if (input !== target) input.value = value.trim(); }); updateAIProfileSummary(); updateExamMessage("模型名称已保存", "#10b981"); setTimeout(() => { const aiType = normalizeAIType(CONFIG.exam.currentAI); updateExamMessage(`就绪(使用 ${AI_PRESETS[aiType].name})`, "#64748b"); }, 2e3); Logger.info(`模型名称: ${value.trim()}`); break; case "exam-delay": CONFIG.exam.delay = parseInt(value) * 1e3; saveConfig(); Logger.info(`答题间隔: ${value}秒`); refreshQuickSettings(); break; case "exam-auto-submit": CONFIG.exam.autoSubmit = value; saveConfig(); Logger.info(`自动交卷: ${value ? "开启" : "关闭"}`); refreshQuickSettings(); break; } } function openWorkbench() { const panel = DOMCache.getById("icve-tabbed-panel"); const workbench = DOMCache.getById("icve-workbench"); if (!panel || !workbench) return; panel.classList.add("is-open"); panel.classList.remove("is-collapsed"); workbench.setAttribute("aria-hidden", "false"); localStorage.setItem("icve_workbench_open", "true"); requestAnimationFrame(() => ensurePanelInViewport()); } function closeWorkbench() { const panel = DOMCache.getById("icve-tabbed-panel"); const workbench = DOMCache.getById("icve-workbench"); if (!panel || !workbench) return; panel.classList.remove("is-open"); panel.classList.add("is-collapsed"); workbench.setAttribute("aria-hidden", "true"); localStorage.setItem("icve_workbench_open", "false"); requestAnimationFrame(() => ensurePanelInViewport()); } function switchTask(task) { const panel = DOMCache.getById("icve-tabbed-panel"); if (!panel) return; panel.dataset.task = task; panel.querySelectorAll("[data-task-panel]").forEach((taskPanel) => { taskPanel.hidden = taskPanel.dataset.taskPanel !== task; }); panel.querySelectorAll("[data-task-select]").forEach((button) => { button.classList.toggle("active", button.dataset.taskSelect === task); }); updateLauncherStatus(); } function saveCurrentAIInputs() { const aiType = normalizeAIType(CONFIG.exam.currentAI); CONFIG.exam.currentAI = aiType; const apiKeyInput = document.querySelector("#exam-api-key"); const apiUrlInput = document.querySelector("#exam-api-url"); const modelInput = document.querySelector("#exam-api-model-name"); if (apiKeyInput) { GM_setValue(`ai_key_${aiType}`, apiKeyInput.value.trim()); } if (apiUrlInput) { GM_setValue(`ai_baseurl_${aiType}`, apiUrlInput.value.trim()); } if (modelInput) { GM_setValue(`ai_model_${aiType}`, modelInput.value.trim()); } saveConfig(); } function updateAIProfileSummary() { const aiType = normalizeAIType(CONFIG.exam.currentAI); const preset = AI_PRESETS[aiType]; const aiConfig = getAIConfig(); const serviceEls = document.querySelectorAll("#exam-ai-service-name"); const modelEls = document.querySelectorAll("#exam-ai-service-model"); const configPills = document.querySelectorAll(".config-pill"); serviceEls.forEach((serviceEl) => { serviceEl.textContent = preset.name; }); configPills.forEach((configPill) => { configPill.textContent = preset.name; }); modelEls.forEach((modelEl) => { const baseURL = aiConfig.baseURL || "未配置地址"; const model = aiConfig.model || "未配置模型"; modelEl.textContent = model; modelEl.title = baseURL; }); refreshQuickSettings(); } function refreshQuickSettings() { const learningQuick = document.querySelector('[data-task-panel="learning"] .quick-settings'); if (learningQuick) { learningQuick.innerHTML = `
倍速${CONFIG.learning.playbackRate}x
等待${CONFIG.learning.waitTimeAfterComplete} 秒
静音${CONFIG.learning.muteMedia ? "开" : "关"}
`; } const examQuick = document.querySelector('[data-task-panel="exam"] .quick-settings'); if (examQuick) { const aiType = normalizeAIType(CONFIG.exam.currentAI); const preset = AI_PRESETS[aiType]; examQuick.innerHTML = `
服务${preset.name}
延迟${CONFIG.exam.delay / 1e3} 秒
交卷${CONFIG.exam.autoSubmit ? "自动" : "手动"}
`; } } function updateLauncherStatus() { var _a; const launcherMeta = document.querySelector("#icve-launcher .launcher-meta"); const launcherDot = document.querySelector("#icve-launcher .launcher-dot"); if (!launcherMeta || !launcherDot) return; launcherDot.classList.remove("running", "error"); if (state.learning.isRunning) { launcherMeta.textContent = `学习中 ${state.learning.completedCount}/${state.learning.totalCount}`; launcherDot.classList.add("running"); return; } if (state.exam.isRunning) { launcherMeta.textContent = `答题中 ${state.exam.currentQuestionIndex}/${state.exam.totalQuestions}`; launcherDot.classList.add("running"); return; } const task = ((_a = document.getElementById("icve-tabbed-panel")) == null ? void 0 : _a.dataset.task) || getDefaultTask(getPageType()); const idleText = { learning: "学习就绪", exam: "答题就绪", config: "配置" }; launcherMeta.textContent = idleText[task] || "学习就绪"; } function toggleApiKeyVisibility() { const apiKeyInputs = document.querySelectorAll("#exam-api-key"); const toggleButtons = document.querySelectorAll("#exam-toggle-api-key"); const firstInput = apiKeyInputs[0]; if (!firstInput) return; const shouldShow = firstInput.type === "password"; apiKeyInputs.forEach((input) => { input.type = shouldShow ? "text" : "password"; }); toggleButtons.forEach((button) => { button.textContent = shouldShow ? "隐藏" : "显示"; button.title = shouldShow ? "隐藏密钥" : "显示密钥"; }); } function toggleTheme() { CONFIG.theme = CONFIG.theme === "light" ? "dark" : "light"; applyTheme(CONFIG.theme); saveConfig(); } function applyTheme(theme) { const panel = DOMCache.getById("icve-tabbed-panel"); const themeBtn = DOMCache.getById("theme-toggle"); if (panel) { if (theme === "dark") { panel.classList.add("dark-theme"); } else { panel.classList.remove("dark-theme"); } } if (themeBtn) { themeBtn.textContent = theme === "dark" ? "☀️" : "🌙"; themeBtn.title = theme === "dark" ? "切换到浅色模式" : "切换到深色模式"; } } function makeDraggable() { const panel = DOMCache.getById("icve-tabbed-panel"); const header = DOMCache.getById("panel-header"); const launcher = DOMCache.getById("icve-launcher"); if (!panel || !header) return; let isDragging = false; let initialX = 0; let initialY = 0; let startX = 0; let startY = 0; let hasMoved = false; let dragSource = null; const dragThreshold = 4; restorePanelPosition(); const startDrag = (e, source) => { if (source === "header" && e.target.closest("button")) return; const rect = panel.getBoundingClientRect(); initialX = e.clientX - rect.left; initialY = e.clientY - rect.top; startX = e.clientX; startY = e.clientY; isDragging = true; hasMoved = false; dragSource = source; panel.style.transition = "none"; }; header.addEventListener("mousedown", (e) => startDrag(e, "header")); launcher == null ? void 0 : launcher.addEventListener("mousedown", (e) => startDrag(e, "launcher")); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; if (!hasMoved && Math.hypot(deltaX, deltaY) < dragThreshold) return; e.preventDefault(); hasMoved = true; const panelRect = panel.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let newX = e.clientX - initialX; let newY = e.clientY - initialY; const minVisibleX = dragSource === "launcher" ? Math.min(180, panelRect.width) : 50; const minVisibleY = dragSource === "launcher" ? Math.min(48, panelRect.height) : 50; const maxX = viewportWidth - minVisibleX; const maxY = viewportHeight - minVisibleY; const minX = minVisibleX - panelRect.width; const minY = 0; newX = Math.max(minX, Math.min(newX, maxX)); newY = Math.max(minY, Math.min(newY, maxY)); panel.style.left = newX + "px"; panel.style.top = newY + "px"; panel.style.right = "auto"; }); document.addEventListener("mouseup", () => { if (isDragging) { if (hasMoved) { if (dragSource === "launcher") { suppressLauncherOpen = true; window.setTimeout(() => { suppressLauncherOpen = false; }, 250); } savePanelPosition(); } panel.style.transition = ""; } isDragging = false; dragSource = null; }); window.addEventListener("resize", Utils.debounce(() => { ensurePanelInViewport(); }, 200)); } function savePanelPosition() { const panel = DOMCache.getById("icve-tabbed-panel"); if (!panel) return; const rect = panel.getBoundingClientRect(); const position = { left: rect.left, top: rect.top, timestamp: Date.now() }; localStorage.setItem("icve_panel_position", JSON.stringify(position)); } function restorePanelPosition() { const panel = DOMCache.getById("icve-tabbed-panel"); if (!panel) return; try { const saved = localStorage.getItem("icve_panel_position"); if (!saved) return; const position = JSON.parse(saved); if (Date.now() - position.timestamp > 7 * 24 * 60 * 60 * 1e3) { localStorage.removeItem("icve_panel_position"); return; } panel.style.left = position.left + "px"; panel.style.top = position.top + "px"; panel.style.right = "auto"; requestAnimationFrame(() => { ensurePanelInViewport(); }); } catch { } } function ensurePanelInViewport() { const panel = DOMCache.getById("icve-tabbed-panel"); if (!panel) return; const rect = panel.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const isCollapsed = panel.classList.contains("is-collapsed"); const minVisibleX = isCollapsed ? Math.min(180, rect.width) : 50; const minVisibleY = isCollapsed ? Math.min(48, rect.height) : 50; let needsUpdate = false; let newLeft = rect.left; let newTop = rect.top; if (rect.left > viewportWidth - minVisibleX) { newLeft = viewportWidth - minVisibleX; needsUpdate = true; } if (rect.right < minVisibleX) { newLeft = minVisibleX - rect.width; needsUpdate = true; } if (rect.top > viewportHeight - minVisibleY) { newTop = viewportHeight - minVisibleY; needsUpdate = true; } if (rect.top < 0) { newTop = 0; needsUpdate = true; } if (needsUpdate) { panel.style.left = newLeft + "px"; panel.style.top = newTop + "px"; panel.style.right = "auto"; savePanelPosition(); } } window.updateRecentEvents = updateRecentEvents; function init() { createPanel(); Logger.info("智慧职教全能助手已加载"); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { setTimeout(init, 1e3); }); } else { setTimeout(init, 1e3); } })();