// ==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 = `
${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 = `
`;
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 `
倍速${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 `
模型
${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 `
`;
}
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);
}
})();