AutoSpeexx

自动完成 Speexx 平台视频播放、练习答题、发音练习跳过、跨页自动续跑,可设定参数模拟人工做题时长。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         AutoSpeexx
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  自动完成 Speexx 平台视频播放、练习答题、发音练习跳过、跨页自动续跑,可设定参数模拟人工做题时长。
// @author       0x9c5
// @match        https://portal.speexx.cn/articles/*
// @grant        GM_addStyle
// @run-at       document-start
// @license      GPL-3.0-only
// ==/UserScript==

(function() {
	'use strict';

	// -------------------------- 1. 核心配置 --------------------------
	const MIN_CONFIG = {
		MIN_TOTAL_TIME: 30000, // 单题最短时间最小值(30秒)
		MIN_RANDOM_DELAY: 1000 // 随机延迟最小值(1000ms)
	};

	const DEFAULT_CONFIG = {
        // 基本配置
		MIN_TOTAL_TIME: 30000, // 单题最短完成时间(默认30秒)
        PRON_WAIT: false, // 是否在发音页面增加额外停留时间
		// 高级配置
		SOLVE_DELAY: 10000, // 显示答案后延迟
		SUBMIT_DELAY: 10000, // 提交后延迟
		NEXT_LOAD_DELAY: 3000, // Next加载延迟
		SCORE_CHECK_DELAY: 2000, // 成绩检测延迟
        MAX_RANDOM_DELAY: 5000, // 随机延迟最大值(默认5秒)
	};
	let CONFIG = {
		...DEFAULT_CONFIG
	};

	// -------------------------- 2. 前端样式 --------------------------
	GM_addStyle(`
        /* 主面板 */
        .autospeexx-panel {
            max-height: 650px; /* 或者 70vh ? */
            display: flex;
            flex-direction: column;
            overflow: hidden;
            position: fixed;
            top: 20px;
            right: 20px;
            width: 400px;
            background: #fff !important;
            border: 1px solid #ccc;
            border-radius: 8px;
            padding: 12px;
            box-shadow: 0 2px 15px rgba(0,0,0,0.2);
            z-index: 999999;
            font-family: Arial, sans-serif;
            font-size: 14px;
            color: #000 !important;
        }
        .autospeexx-panel, .autospeexx-panel * {
            box-sizing: border-box;
        }

        /* 表头&拖动 */
        .autospeexx-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-weight: bold;
            margin-bottom: 10px;
            padding-bottom: 8px;
            border-bottom: 1px solid #eee;
            cursor: move;
            user-select: none;
        }
        .autospeexx-title { font-size: 16px; }
        .autospeexx-min-btn {
            background: none;
            border: none;
            font-size: 18px;
            cursor: pointer;
            color: #666 !important;
            padding: 0 5px;
        }

        /* 分页 */
        .autospeexx-tabs {
            display: flex;
            margin-bottom: 10px;
            border-bottom: 1px solid #eee;
            padding-bottom: 8px;
        }
        .autospeexx-tab {
            padding: 6px 12px;
            cursor: pointer;
            border-radius: 4px;
            margin-right: 8px;
            color: #000 !important;
        }
        .autospeexx-tab.active {
            background: #ff7700 !important;
            color: #fff !important;
        }
        .autospeexx-tab:not(.active):hover {
            background: #f5f5f5 !important;
        }
        .autospeexx-tab-content {
            display: none;
            color: #000 !important;
        }
        .autospeexx-tab-content.active {
            display: block;
        }

        /* 悬浮球(左下角) */
        .autospeexx-float-ball {
            position: fixed;
            bottom: 20px;
            left: 20px;
            width: 60px;
            height: 60px;
            background: #ff7700 !important;
            border-radius: 50%;
            color: #fff !important;
            display: none;
            justify-content: center;
            align-items: center;
            cursor: pointer;
            z-index: 999999;
        }
        .autospeexx-float-ball::before {
            content: "🚀";
            font-size: 30px;
        }

        /* 按钮 */
        .autospeexx-btn {
            padding: 6px 12px;
            margin: 5px 0;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            width: 100%;
        }
        .autospeexx-btn-start { background: #28a745 !important; color: #fff !important; }
        .autospeexx-btn-start:disabled { background: #6c757d !important; }
        .autospeexx-btn-stop { background: #dc3545 !important; color: #fff !important; }
        .autospeexx-btn-stop:disabled { background: #6c757d !important; }
        .autospeexx-btn-save { background: #007bff !important; color: #fff !important; margin-right: 5px; }
        .autospeexx-btn-reset { background: #6c757d !important; color: #fff !important; }

        /* 参数配置 */
        .autospeexx-setting-item {
            margin: 10px 0;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        .autospeexx-setting-label {
            font-size: 13px;
            color: #333 !important;
            width: 180px;
        }
        .autospeexx-setting-input {
            width: 100px;
            padding: 4px 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 13px;
        }

        /* 中间滚动容器 */
        .autospeexx-scroll-container {
            flex-grow: 1;
            overflow-y: scroll;
            padding: 6px;
            border-bottom: 1px solid #eee;
        }

        /*  优化滚动条样式 */
        .autospeexx-scroll-container::-webkit-scrollbar {
            width: 6px;
        }
        .autospeexx-scroll-container::-webkit-scrollbar-thumb {
            background: #ddd;
            border-radius: 10px;
        }
        .autospeexx-scroll-container::-webkit-scrollbar-thumb:hover {
            background: #ccc;
        }

        /* advanced-settings 标题样式 */
        /* 隐藏所有页面可能自带的默认箭头 */
        .autospeexx-advanced-settings summary {
            list-style: none !important;
            outline: none !important;
            cursor: pointer;
            position: relative;
            padding-left: 20px !important; /* 为自定义箭头留位 */
            user-select: none;
            color: #666;
            font-size: 13px;
            margin: 10px 0;
        }

        .autospeexx-advanced-settings summary::-webkit-details-marker {
            display: none !important; /* Chrome/Safari */
        }

        /* 伪元素绘制箭头 */
        .autospeexx-advanced-settings summary::before {
            content: '▶';
            position: absolute;
            left: 0;
            top: 50%;
            transform: translateY(-50%);
            font-size: 10px;
            color: #dc3545;
            transition: transform 0.2s ease;
        }

        /* 展开时,让箭头旋转 90 度变成向下 */
        .autospeexx-advanced-settings[open] summary::before {
            transform: translateY(-50%) rotate(90deg);
        }

        /* 状态 & 日志 */
        .autospeexx-page-type { font-size: 12px; color: #007bff !important; margin: 5px 0; }
        .autospeexx-tip { font-size: 12px; color:rgb(128, 128, 128) !important; margin: 5px 0; }
        .autospeexx-log {
            flex-shrink: 0;
            height: 160px;  /* 100px → 160px */
            border: 1px solid #eee;
            padding: 8px;
            margin-top: 10px;
            font-size: 12px;
            overflow-y: auto;
            background: #f0f0f0 !important;
            white-space: pre-wrap;
        }

        /* 最小化 */
        .autospeexx-panel.minimized { display: none !important; }
    `);

	// -------------------------- 3. 全局变量 --------------------------
	let isRunning = false;
	let isMinimized = false;
	let currentTaskTimer = null;
	let currentPageType = 'unknown';
	let logList = [];
	const STORAGE_KEY = 'AutoSpeexxRunning';
	const CONFIG_STORAGE_KEY = 'AutoSpeexxConfig';

	// -------------------------- 4. 工具函数 --------------------------
	// 打印日志显示运行状态
	function addLog(msg) {
		const time = new Date().toLocaleTimeString();
		const logItem = `[${time}] ${msg}`;
		logList.unshift(logItem);
		if (logList.length > 20) logList.pop();
		const logDoms = document.querySelectorAll('.autospeexx-log');
		logDoms.forEach(dom => {
			dom.innerHTML = logList.join('<br>');
		});
		console.log(`[Speexx] ${logItem}`);
	}

	function loadConfig() {
		const savedConfig = localStorage.getItem(CONFIG_STORAGE_KEY);
		if (savedConfig) {
			try {
				const parsed = JSON.parse(savedConfig);
				// 基本配置
				CONFIG.MIN_TOTAL_TIME = Math.max(parsed.MIN_TOTAL_TIME || DEFAULT_CONFIG.MIN_TOTAL_TIME, MIN_CONFIG.MIN_TOTAL_TIME);
                if (typeof parsed.PRON_WAIT === 'boolean') {CONFIG.PRON_WAIT = parsed.PRON_WAIT;} else {CONFIG.PRON_WAIT = DEFAULT_CONFIG.PRON_WAIT;}

				// 高级配置
				CONFIG.SOLVE_DELAY = (typeof parsed.SOLVE_DELAY === 'number' && parsed.SOLVE_DELAY > 0) ? parsed.SOLVE_DELAY : DEFAULT_CONFIG.SOLVE_DELAY;
				CONFIG.SUBMIT_DELAY = (typeof parsed.SUBMIT_DELAY === 'number' && parsed.SUBMIT_DELAY > 0) ? parsed.SUBMIT_DELAY : DEFAULT_CONFIG.SUBMIT_DELAY;
				CONFIG.NEXT_LOAD_DELAY = (typeof parsed.NEXT_LOAD_DELAY === 'number' && parsed.NEXT_LOAD_DELAY > 0) ? parsed.NEXT_LOAD_DELAY : DEFAULT_CONFIG.NEXT_LOAD_DELAY;
				CONFIG.SCORE_CHECK_DELAY = (typeof parsed.SCORE_CHECK_DELAY === 'number' && parsed.SCORE_CHECK_DELAY > 0) ? parsed.SCORE_CHECK_DELAY : DEFAULT_CONFIG.SCORE_CHECK_DELAY;
                CONFIG.MAX_RANDOM_DELAY = Math.max(parsed.MAX_RANDOM_DELAY || DEFAULT_CONFIG.MAX_RANDOM_DELAY, MIN_CONFIG.MIN_RANDOM_DELAY);

				addLog('✅ 配置加载完成');
			} catch (e) {
				addLog(`❌ 配置加载失败:${e.message}`);
			}
		}
		updateConfigInputs();
	}

	function saveConfig() {
		// 基本配置
		const minTotalTime = Math.max(Number(document.getElementById('minTotalTimeInput').value) || DEFAULT_CONFIG.MIN_TOTAL_TIME, MIN_CONFIG.MIN_TOTAL_TIME);
        const pronWait = document.getElementById('pronWaitInput') ? document.getElementById('pronWaitInput').checked : DEFAULT_CONFIG.PRON_WAIT;

		// 高级配置
		const solveDelay = (Number(document.getElementById('solveDelayInput').value) > 0) ? Number(document.getElementById('solveDelayInput').value) : DEFAULT_CONFIG.SOLVE_DELAY;
		const submitDelay = (Number(document.getElementById('submitDelayInput').value) > 0) ? Number(document.getElementById('submitDelayInput').value) : DEFAULT_CONFIG.SUBMIT_DELAY;
		const nextLoadDelay = (Number(document.getElementById('nextLoadDelayInput').value) > 0) ? Number(document.getElementById('nextLoadDelayInput').value) : DEFAULT_CONFIG.NEXT_LOAD_DELAY;
		const scoreCheckDelay = (Number(document.getElementById('scoreCheckDelayInput').value) > 0) ? Number(document.getElementById('scoreCheckDelayInput').value) : DEFAULT_CONFIG.SCORE_CHECK_DELAY;
        const maxRandomDelay = Math.max(Number(document.getElementById('maxRandomDelayInput').value) || DEFAULT_CONFIG.MAX_RANDOM_DELAY, MIN_CONFIG.MIN_RANDOM_DELAY);

		// 更新配置
		CONFIG.MIN_TOTAL_TIME = minTotalTime;
        CONFIG.PRON_WAIT = pronWait;
		CONFIG.SOLVE_DELAY = solveDelay;
		CONFIG.SUBMIT_DELAY = submitDelay;
		CONFIG.NEXT_LOAD_DELAY = nextLoadDelay;
		CONFIG.SCORE_CHECK_DELAY = scoreCheckDelay;
        CONFIG.MAX_RANDOM_DELAY = maxRandomDelay;

		// 保存到本地存储
		localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify({
			MIN_TOTAL_TIME: CONFIG.MIN_TOTAL_TIME,
            PRON_WAIT: CONFIG.PRON_WAIT,
			SOLVE_DELAY: CONFIG.SOLVE_DELAY,
			SUBMIT_DELAY: CONFIG.SUBMIT_DELAY,
			NEXT_LOAD_DELAY: CONFIG.NEXT_LOAD_DELAY,
			SCORE_CHECK_DELAY: CONFIG.SCORE_CHECK_DELAY,
            MAX_RANDOM_DELAY: CONFIG.MAX_RANDOM_DELAY
		}));

		updateConfigInputs();
		addLog(`⚙️ 配置保存成功:
基本配置:单题最短${CONFIG.MIN_TOTAL_TIME/1000}秒完成,最大随机延迟${CONFIG.MAX_RANDOM_DELAY/1000}秒,发音页面更长停留 [${CONFIG.PRON_WAIT ? '开启' : '关闭'}]
高级配置:显示答案延迟${CONFIG.SOLVE_DELAY/1000}秒,提交延迟${CONFIG.SUBMIT_DELAY/1000}秒,跨页加载预留延迟${CONFIG.NEXT_LOAD_DELAY/1000}秒,成绩检测延迟${CONFIG.SCORE_CHECK_DELAY/1000}秒`);
	}

	function resetConfig() {
		// 重置为默认值(含高级配置)
		CONFIG.MIN_TOTAL_TIME = DEFAULT_CONFIG.MIN_TOTAL_TIME;
        CONFIG.PRON_WAIT = DEFAULT_CONFIG.PRON_WAIT;
		CONFIG.SOLVE_DELAY = DEFAULT_CONFIG.SOLVE_DELAY;
		CONFIG.SUBMIT_DELAY = DEFAULT_CONFIG.SUBMIT_DELAY;
		CONFIG.NEXT_LOAD_DELAY = DEFAULT_CONFIG.NEXT_LOAD_DELAY;
		CONFIG.SCORE_CHECK_DELAY = DEFAULT_CONFIG.SCORE_CHECK_DELAY;
        CONFIG.MAX_RANDOM_DELAY = DEFAULT_CONFIG.MAX_RANDOM_DELAY;

		localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify({
			MIN_TOTAL_TIME: CONFIG.MIN_TOTAL_TIME,
            PRON_WAIT: CONFIG.PRON_WAIT,
			SOLVE_DELAY: CONFIG.SOLVE_DELAY,
			SUBMIT_DELAY: CONFIG.SUBMIT_DELAY,
			NEXT_LOAD_DELAY: CONFIG.NEXT_LOAD_DELAY,
			SCORE_CHECK_DELAY: CONFIG.SCORE_CHECK_DELAY,
            MAX_RANDOM_DELAY: CONFIG.MAX_RANDOM_DELAY
		}));

		updateConfigInputs();
		addLog('⚙️ 配置重置为默认值');
	}

	function updateConfigInputs() {
		// 基本配置输入框更新
		const minTotalTimeInput = document.getElementById('minTotalTimeInput');
        const pronWaitInput = document.getElementById('pronWaitInput');

		if (minTotalTimeInput) minTotalTimeInput.value = CONFIG.MIN_TOTAL_TIME;
        pronWaitInput.checked = !!CONFIG.PRON_WAIT;

		// 高级配置输入框更新
		const solveDelayInput = document.getElementById('solveDelayInput');
		const submitDelayInput = document.getElementById('submitDelayInput');
		const nextLoadDelayInput = document.getElementById('nextLoadDelayInput');
		const scoreCheckDelayInput = document.getElementById('scoreCheckDelayInput');
        const maxRandomDelayInput = document.getElementById('maxRandomDelayInput');

		if (solveDelayInput) solveDelayInput.value = CONFIG.SOLVE_DELAY;
		if (submitDelayInput) submitDelayInput.value = CONFIG.SUBMIT_DELAY;
		if (nextLoadDelayInput) nextLoadDelayInput.value = CONFIG.NEXT_LOAD_DELAY;
		if (scoreCheckDelayInput) scoreCheckDelayInput.value = CONFIG.SCORE_CHECK_DELAY;
        if (maxRandomDelayInput) maxRandomDelayInput.value = CONFIG.MAX_RANDOM_DELAY;
	}

	// 生成随机延迟(1000ms ~ 最大值)
	function getRandomDelay() {
		return Math.floor(Math.random() * (CONFIG.MAX_RANDOM_DELAY - MIN_CONFIG.MIN_RANDOM_DELAY)) + MIN_CONFIG.MIN_RANDOM_DELAY;
	}

	// 检测结算界面(.graphic-stats)
	function isResultPage() {
		return !!document.querySelector('.graphic-stats');
	}

	// 动态检测页面类型(/magazine 和 /vocabulary-trainer 统一归为 unknown)
	function detectPageType() {
		const url = window.location.href;
		let type = 'unknown';

		// 页面类型判断
		if (document.querySelector('.graphic-stats')) {
			type = 'result';
		} else if (document.querySelector('.microphone-pulse') || url.includes('/pronunciation')) {
			type = 'pronunciation';
		} else if (document.querySelector('.vjs-big-play-button') || url.includes('/video')) {
			type = 'video';
		} else if (window.location.href.includes('/exercise') || document.querySelector('.exercise-content')) {
			type = 'exercise';
		} else if (document.querySelector('[data-testid="daily-practice-container"]') || document.querySelector('[aria-label="主页"]')) {
			type = 'home';
		} else if (url.includes('/magazine') || url.includes('/vocabulary')) {
			type = 'unknown';
		}

		// 更新页面类型和日志(保留)
		if (type !== currentPageType) {
			addLog(`🔄 页面类型:${currentPageType} → ${type}`);
			currentPageType = type;
			const pageTypeDoms = document.querySelectorAll('.autospeexx-page-type');
			pageTypeDoms.forEach(dom => {
				dom.textContent = `当前页面:${type}`;
			});
		}
		return type;
	}

	// 检测题目加载 / 下一题 / 成绩显示
	function isExerciseLoaded() {
		return !!document.querySelector('.exercise-content') || !!document.querySelector('.question');
	}

	// 是否存在 Next 按钮
	function hasNextQuestion() {
		const nextBtn = document.querySelector('.next');
		return nextBtn && !nextBtn.disabled;
	}

	// 检测显示成绩的 <span> 元素是否存在(无分数校验)
	function isScoreDisplayed() {
		return !!document.querySelector('span[class*="result"]') || !!document.querySelector('span[class*="result-badge-container"]') || !!document.querySelector('.next:not(:disabled)');
	}

	// 初始化练习组件
	function initExerciseComponent() {
		try {
			if (!window.entryp) {
				window.entryp = {
					trigger: function(action) {
						// 视频页跳过提交触发,避免报错
						if (currentPageType === 'video' && action === 'correct') {
							addLog('⚠️ 视频页跳过correction触发(避免JS报错)');
							return;
						}
						const btn = document.querySelector(`.btn-${action}`) || document.querySelector(`[data-action="${action}"]`);
						if (btn) btn.click();
					}
				};
			}

			// --- BEGIN: Backbone Interception Logic ---
            // Part of this section is derived from speexx-auto by BeautyyuYanli
            // Licensed under GPL-3.0. Source: https://github.com/NAOSI-DLUT/speexx-auto
            // ---
			CourseWare.CourseExercises.CourseExercisesControlsView = Backbone.Speexx.HandlebarsView.extend({
				templateName: "cw-language-course-controls",
				className: "exercise-controls",
				initialize: function(e) {
					var t = this.exerciseView = e.exerciseView,
						n = t.model;
					this.firstTime = !0;
					this.solutionShown = !1;
					this.modified = !1;
					this.dialogEnded = !1;

					this.listenTo(t, "modified", () => {
						this.modified || (this.modified = !0, this.render());
					});
					this.listenToOnce(t, "dialog:ended", () => {
						this.dialogEnded = !0, this.render();
					});
					this.listenTo(t, "static:added static:removed", () => {
						this.render();
					});
					this.listenTo(n, "sync", (e, t, n) => {
						var r = !!this.silent;
						this.silent = !!n.silent;
						(!this.silent || !r) && this.render();
					});
					this.listenTo(CourseWare.Language, "change:textpool", () => {
						this.render();
					});
					this.on("render:after", () => {
						this.$(".btn.btn-link[title]").tooltip();
					});

					window.entryp = this;
				},
				templateModel: function() {
					var e = this.exerciseView,
						t = e.model,
						n = this.silent ? null : t.get("result");
					var r = t.get("type").pronunciation,
						i = !e.static && t.get("type").hasResult && !r;
					var s = e.static || !t.get("type").hasResult,
						o = !this.solutionShown && i && !this.firstTime && n !== 100;
					return {
						firstTime: this.firstTime,
						hasCorrect: i,
						hasSolution: o,
						hideResult: s,
						dialogEnded: this.dialogEnded,
						modified: this.modified,
						solutionShown: this.solutionShown,
						result: n
					};
				},
				events: {
					"click .btn.correct": function() {
						CourseWare.Audio.stop();
						this.trigger("correct");
					},
					"click .btn.next": function() {
						this.trigger("next");
					},
					"click .btn.solution": function() {
						this.render();
						this.trigger("solve");
					}
				}
			});
            // --- END: Backbone Interception Logic ---

			addLog('✅ 练习组件初始化完成');
		} catch (e) {
			addLog(`⚠️ 组件初始化异常:${e.message}`);
		}
	}

	// -------------------------- 5. 核心处理逻辑 --------------------------
	function processCurrentPage() {
		if (!isRunning) return;

		const pageType = detectPageType();

		// 页面类型分别处理(新增 pronunciation)
		switch (pageType) {
			case 'result':
				// 结算页面:随机延迟后点击 Next
				processResultPage();
				break;
			case 'pronunciation':
				// 发音页面:一路 Next(每次 Next 前随机延迟)
				processPronunciationPage();
				break;
			case 'video':
				// 视频页面:播放视频逻辑
				processVideoPage();
				break;
			case 'exercise':
				// 练习页面:自动做题逻辑
				processExercisePage();
				break;
				// home/unknown 页面停止自动任务
			case 'home':
			case 'unknown':
				addLog('❌ 当前是主页/其他页面,停止自动任务');
				stopTask();
				break;
		}
	}

	function processResultPage() {
		addLog('✅ 检测到结算界面,随机延迟后触发Next');
		// 生成随机延迟
		const randomDelay = getRandomDelay();
		addLog(`⌛ 结算页随机延迟${randomDelay/1000}秒`);

		setTimeout(() => {
			if (!isRunning) return;

			// 点击 Next 按钮
			const nextBtn = document.querySelector('.next');
			if (nextBtn && !nextBtn.disabled) {
				nextBtn.click();
				addLog('✅ 结算页触发Next,进入下一页');

				// 防止第一题空题 
				// 清除所有残留定时器,防止提前执行逻辑
				if (currentTaskTimer) {
					clearTimeout(currentTaskTimer);
					currentTaskTimer = null;
				}
				// 强制等待页面加载(延长等待逻辑)
				addLog(`⌛ 强制等待${CONFIG.NEXT_LOAD_DELAY/1000}秒,等待新页面完全加载`);
				currentTaskTimer = setTimeout(() => {
					if (isRunning) {
						processCurrentPage(); // 等待结束后再检测页面
					}
				}, CONFIG.NEXT_LOAD_DELAY);
			} else {
				addLog('⚠️ 结算页未找到可用的Next按钮,任务完成');
				stopTask();
			}
		}, randomDelay);
	}

	// 处理发音页面(仅需一路 Next,每次触发前随机延迟)
	function processPronunciationPage() {
		addLog('✅ 检测到发音练习页面,启用自动跳过逻辑');

		// 将原来的 stopTask() 改为延迟重试
		if (!hasNextQuestion()) {
			addLog('⌛ 发音页面Next按钮未就绪,2秒后重试...');
			currentTaskTimer = setTimeout(processPronunciationPage, 2000);
			return;
		}

		// 生成随机延迟
		const randomDelay = getRandomDelay();
		addLog(`⌛ 延迟${randomDelay * (3 ** CONFIG.PRON_WAIT)/1000}秒后点击Next`);

		setTimeout(() => {
			if (!isRunning) return;
			const nextBtn = document.querySelector('.next');
			if (nextBtn && !nextBtn.disabled) {
				nextBtn.click();
				addLog('✅ 触发Next,进入下一页');
				setTimeout(processCurrentPage, CONFIG.NEXT_LOAD_DELAY);
			} else {
				addLog('⚠️ 发音页面Next按钮不可用,任务停止');
				stopTask();
			}
		}, randomDelay * (3 ** CONFIG.PRON_WAIT));
	}

	// 处理视频页面
	function processVideoPage() {
		addLog('✅ 检测到视频页面,启用视频播放逻辑');

		// 播放视频
		const playBtn = document.querySelector('.vjs-big-play-button') || document.querySelector('.video-play-btn');
		if (playBtn) {
			playBtn.click();
			addLog('▶️ 播放视频');
		} else {
			addLog('⚠️ 未找到播放按钮,假设视频已播放');
		}

		// 检测视频播放完成
		const checkVideoComplete = () => {
			if (!isRunning) return;

			// 检测视频完成状态
			let isComplete = false;
			const videoElement = document.querySelector('video');
			const timeDisplay = document.querySelector('.vjs-remaining-time-display');

			// 1:检测剩余时间
			if (timeDisplay && timeDisplay.textContent.trim() === '0:00') {
				isComplete = true;
			}
			// 2:检测 .ended
			else if (videoElement && videoElement.ended) {
				isComplete = true;
			}
			// 3:检测视频完成标识
			else if (document.querySelector('.video-complete') || document.querySelector('.lesson-complete')) {
				isComplete = true;
			}

			if (isComplete) {
				addLog('✅ 视频播放完成');
				// 随机延迟后点击 Next
				const randomDelay = getRandomDelay();
				addLog(`✅ 视频完成后随机延迟${randomDelay/1000}秒`);

				setTimeout(() => {
					if (!isRunning) return;

					const nextBtn = document.querySelector('.next');
					if (nextBtn && !nextBtn.disabled) {
						nextBtn.click();
						addLog('✅ 视频页点击Next,进入下一页');
						setTimeout(processCurrentPage, CONFIG.NEXT_LOAD_DELAY);
					} else {
						addLog('⚠️ 视频页未找到可用的Next按钮,任务结束');
						stopTask();
					}
				}, randomDelay);
			} else {
				// 1 秒后再次检测
				currentTaskTimer = setTimeout(checkVideoComplete, 1000);
			}
		};

		// 启动视频完成检测
		checkVideoComplete();
	}

	// 处理练习页面(自动做题逻辑)
	function processExercisePage() {
		// 再次检测结算界面
		if (isResultPage()) {
			processResultPage();
			return;
		}

		// --- 新增:针对存在 Start 按钮的特殊题型的处理 ---
		const startBtn = document.querySelector('.btn-primary.start-exercise');
		if (startBtn) {
			addLog('🔎 检测到前置 Start 按钮,尝试激活练习...');
			startBtn.click();
			// 点击后等待 1.5 秒让题目加载,然后重新进入当前页逻辑
			currentTaskTimer = setTimeout(processCurrentPage, 1500);
			return;
		}
		// -----------------------------------------------

		// 等待题目加载 + 强制初始化
		if (!isExerciseLoaded()) {
			addLog('⚠️ 题目未加载,3秒后重试');
			initExerciseComponent();
			currentTaskTimer = setTimeout(processExercisePage, 3000);
			return;
		}

		// 记录开始时间
		const startTime = Date.now();
		addLog(`✅ 开始处理题目(最短${CONFIG.MIN_TOTAL_TIME/1000}秒)`);

		// 1. 显示答案
		if (window.entryp) {
			window.entryp.trigger("solve");
			addLog(`✅ 显示答案,等待${CONFIG.SOLVE_DELAY/1000}秒`);
		}

		// 2. 延迟提交
		setTimeout(() => {
			if (!isRunning) return;

			if (window.entryp) {
				window.entryp.trigger("correct");
				addLog(`✅ 提交答案,等待${CONFIG.SUBMIT_DELAY/1000}秒`);
			}

			// 3. 成绩检测 + 计算总耗时
			setTimeout(() => {
				if (!isRunning) return;

				addLog('✅ 检测成绩显示');
				if (!isScoreDisplayed()) {
					addLog(`⚠️ 未检测到成绩显示,等待${CONFIG.SCORE_CHECK_DELAY/1000}秒`);
					setTimeout(proceedToNext, CONFIG.SCORE_CHECK_DELAY);
				} else {
					proceedToNext();
				}

				function proceedToNext() {
					// 计算已耗时
					const elapsedTime = Date.now() - startTime;
					// 补充延迟仅补到最短时间
					const needWaitTime = Math.max(0, CONFIG.MIN_TOTAL_TIME - elapsedTime);

					if (needWaitTime > 0) {
						addLog(`⌛ 补充延迟${needWaitTime/1000}秒(补至单题最短时间)`);
						setTimeout(() => {
							addRandomDelayThenNext();
						}, needWaitTime);
					} else {
						addRandomDelayThenNext();
					}
				}

				// 独立的随机延迟步骤
				function addRandomDelayThenNext() {
					const randomDelay = getRandomDelay();
					addLog(`⌛ 随机延迟${randomDelay/1000}秒(独立)`);
					setTimeout(() => {
						clickNext();
					}, randomDelay);
				}

				function clickNext() {
					if (!isRunning) return;

					// 再次检测结算界面
					if (isResultPage()) {
						processResultPage();
						return;
					}

					if (hasNextQuestion()) {
						const nextBtn = document.querySelector('.next');
						nextBtn.click();
						addLog('✅ 练习页点击Next,进入下一题');
						setTimeout(processCurrentPage, CONFIG.NEXT_LOAD_DELAY);
					} else {
						addLog('✅ 练习页无下一题,任务完成');
						stopTask();
					}
				}

			}, CONFIG.SUBMIT_DELAY);

		}, CONFIG.SOLVE_DELAY);
	}

	// -------------------------- 6. 启动/停止任务 --------------------------
	function startTask() {
		if (isRunning) return;

		// 启动前强制检测 + 初始化
		detectPageType();
		initExerciseComponent();

		// 仅在视频 / 练习 / 结算 / 发音页面启动
		if (['video', 'exercise', 'result', 'pronunciation'].indexOf(currentPageType) === -1) {
			addLog('❌ 当前是主页/其他页面,无法启动');
			return;
		}

		// 启动任务
		isRunning = true;
		localStorage.setItem(STORAGE_KEY, 'true');
		document.getElementById('startBtn').disabled = true;
		document.getElementById('stopBtn').disabled = false;
		addLog('===== 自动任务启动 =====');

		// 立即执行一次
		processCurrentPage();
	}

	function stopTask() {
		if (currentTaskTimer) clearTimeout(currentTaskTimer);
		currentTaskTimer = null;

		isRunning = false;
		localStorage.removeItem(STORAGE_KEY);
		document.getElementById('startBtn').disabled = false;
		document.getElementById('stopBtn').disabled = true;
		addLog('===== 自动任务停止 =====');
	}


	// -------------------------- 7. 面板初始化 --------------------------
	// 面板拖动限制
	function makeDraggable(dragTarget, dragHandle) {
		let isDragging = false;
		let offsetX, offsetY;

		dragHandle.addEventListener('mousedown', startDrag);
		document.addEventListener('mousemove', drag);
		document.addEventListener('mouseup', stopDrag);

		function startDrag(e) {
			e.preventDefault();
			isDragging = true;
			const rect = dragTarget.getBoundingClientRect();
			offsetX = e.clientX - rect.left;
			offsetY = e.clientY - rect.top;
			dragTarget.style.zIndex = 999999;
		}

		function drag(e) {
			if (!isDragging) return;
			e.preventDefault();

			let newX = e.clientX - offsetX;
			let newY = e.clientY - offsetY;

			// 边界限制
			const windowWidth = window.innerWidth;
			const windowHeight = window.innerHeight;
			const panelWidth = dragTarget.offsetWidth;
			const panelHeight = dragTarget.offsetHeight;

			newX = Math.max(0, Math.min(newX, windowWidth - panelWidth));
			newY = Math.max(0, Math.min(newY, windowHeight - panelHeight));

			dragTarget.style.left = `${newX}px`;
			dragTarget.style.top = `${newY}px`;
			dragTarget.style.right = 'auto';
			dragTarget.style.bottom = 'auto';
		}

		function stopDrag() {
			isDragging = false;
		}

		// 防止内存泄漏
		dragTarget.addEventListener('remove', () => {
			dragHandle.removeEventListener('mousedown', startDrag);
			document.removeEventListener('mousemove', drag);
			document.removeEventListener('mouseup', stopDrag);
		});
	}

	// 面板初始化
	function initPanel() {
		// 创建面板
		const panel = document.createElement('div');
		panel.className = 'autospeexx-panel';
		panel.innerHTML = `
            <div class="autospeexx-header">
                <div class="autospeexx-title">Auto<span style="color:#ff7700;">Speexx</span></div>
                <button class="autospeexx-min-btn">—</button>
            </div>

            <!-- 分页 -->
            <div class="autospeexx-tabs">
                <div class="autospeexx-tab active" data-tab="main">核心功能</div>
                <div class="autospeexx-tab" data-tab="settings">参数配置</div>
            </div>

            <div class="autospeexx-scroll-container">
                <!-- 核心功能页 -->
                <div class="autospeexx-tab-content active" id="mainTab">
                    <button class="autospeexx-btn autospeexx-btn-start" id="startBtn">启动自动任务</button>
                    <button class="autospeexx-btn autospeexx-btn-stop" id="stopBtn" disabled>停止自动任务</button>
                    <div class="autospeexx-page-type">当前页面:unknown</div>
                    <div class="autospeexx-tip">💡 支持视频自动播放 / 练习自动完成 / 发音自动跳过</div>
                    <div class="autospeexx-tip">💡 出现问题请尝试手动返回后重新启动任务,或者刷新页面</div>
                </div>

                <!-- 参数配置页(新增基本 / 高级配置分割) -->
                <div class="autospeexx-tab-content" id="settingsTab">
                    <!-- 基本配置 -->
                    <div style="font-weight:bold; margin:10px 0; color:#ff7700 !important;">⚙️ 基本配置</div>
                    <div class="autospeexx-setting-item">
                        <label class="autospeexx-setting-label">单题最短完成时间(ms):</label>
                        <input type="number" class="autospeexx-setting-input" id="minTotalTimeInput">
                    </div>
                    <div class="autospeexx-tip">💡 至少设为 30 秒以避免学习时长异常</div>
                    <div class="autospeexx-setting-item">
                        <label class="autospeexx-setting-label">在发音练习页面停留更久:</label>
                        <input type="checkbox" id="pronWaitInput" style="transform: scale(1.2);">
                    </div>
                    <div class="autospeexx-tip">💡 发音练习停留三倍时长(可避免出现 0 分钟学习时间)</div>

                    <!-- 高级配置分割线 -->
                    <hr style="border:0; border-top:2px dashed #eee; margin:15px 0;">
                    <details class="autospeexx-advanced-settings">
                        <summary style="font-weight:bold; margin:10px 0; color:#dc3545 !important;">
                            🔩 高级配置
                        </summary>
                        <div class="autospeexx-tip" style="color: red !important;">⚠️ 若非了解以下参数含义及改动影响,建议保持默认值</div>
                        <!-- 高级配置项 -->
                        <div class="autospeexx-setting-item">
                            <label class="autospeexx-setting-label">显示答案延迟(ms):</label>
                            <input type="number" class="autospeexx-setting-input" id="solveDelayInput">
                        </div>
                        <div class="autospeexx-tip">💡 触发 Solution 后等待的时间</div>
                        <div class="autospeexx-setting-item">
                            <label class="autospeexx-setting-label">提交延迟(ms):</label>
                            <input type="number" class="autospeexx-setting-input" id="submitDelayInput">
                        </div>
                        <div class="autospeexx-tip">💡 调用 this.trigger("solve") 后延迟(模拟填入答案的耗时)</div>
                        <div class="autospeexx-setting-item">
                            <label class="autospeexx-setting-label">跨页加载预留延迟(ms):</label>
                            <input type="number" class="autospeexx-setting-input" id="nextLoadDelayInput">
                        </div>
                        <div class="autospeexx-tip">💡 触发 Next 后预留的加载时间</div>
                        <div class="autospeexx-setting-item">
                            <label class="autospeexx-setting-label">成绩检测延迟(ms):</label>
                            <input type="number" class="autospeexx-setting-input" id="scoreCheckDelayInput">
                        </div>
                        <div class="autospeexx-tip">💡 查询到 [class*="result-badge-container"] 后的延迟</div>
                        <div class="autospeexx-setting-item">
                            <label class="autospeexx-setting-label">最大随机延迟(ms):</label>
                            <input type="number" class="autospeexx-setting-input" id="maxRandomDelayInput">
                        </div>
                        <div class="autospeexx-tip">💡 随机延迟范围:1000ms ~ 最大值(模拟人工操作的随机性)</div>
                    </details>
                    <!-- 按钮区域 -->
                    <div style="margin-top:15px;">
                        <button class="autospeexx-btn autospeexx-btn-save" id="saveConfigBtn" style="width: auto;">保存配置</button>
                        <button class="autospeexx-btn autospeexx-btn-reset" id="resetConfigBtn" style="width: auto;">重置为默认</button>
                    </div>
                </div>
            </div>

            <div class="autospeexx-log">=== 日志显示 ===</div>
        `;

		document.body.appendChild(panel); // 似乎导致 Console 报错但不影响运行

		// 悬浮球(左下角)
		const floatBall = document.createElement('div');
		floatBall.className = 'autospeexx-float-ball';
		document.body.appendChild(floatBall);

		// 最小化 / 恢复
		panel.querySelector('.autospeexx-min-btn').addEventListener('click', () => {
			isMinimized = true;
			panel.classList.add('minimized');
			floatBall.style.display = 'flex';
			addLog('🔽 面板已最小化至左下角');
		});
		floatBall.addEventListener('click', () => {
			isMinimized = false;
			panel.classList.remove('minimized');
			floatBall.style.display = 'none';
			addLog('🔼 面板已恢复');
		});

		// 分页切换
		const tabs = panel.querySelectorAll('.autospeexx-tab');
		const tabContents = panel.querySelectorAll('.autospeexx-tab-content');
		tabs.forEach(tab => {
			tab.addEventListener('click', () => {
				tabs.forEach(t => t.classList.remove('active'));
				tabContents.forEach(c => c.classList.remove('active'));
				tab.classList.add('active');
				const targetTab = document.getElementById(`${tab.dataset.tab}Tab`);
				if (targetTab) targetTab.classList.add('active');
			});
		});

		// 绑定事件
		document.getElementById('startBtn').addEventListener('click', startTask);
		document.getElementById('stopBtn').addEventListener('click', stopTask);
		document.getElementById('saveConfigBtn').addEventListener('click', saveConfig);
		document.getElementById('resetConfigBtn').addEventListener('click', resetConfig);

		// 限制拖动
		makeDraggable(panel, panel.querySelector('.autospeexx-header'));

		// 加载配置
		loadConfig();

		// 启动前强制检测页面类型 + 初始化组件
		detectPageType();
		initExerciseComponent();

		// 续跑恢复
		if (localStorage.getItem(STORAGE_KEY) === 'true') {
			startTask();
		}

		// 延迟检测页面类型(针对低网速 / 主页加载的情况)
		setTimeout(() => {
			detectPageType(); // 延迟后重新检测,网速正常的情况下此时主页元素已加载完成
			addLog('🔍 复检测页面类型');
		}, 1500); // 1.5 秒延迟
	}

	// -------------------------- 8. 初始化 --------------------------
	function waitForDOMReady() {
		if (document.readyState === 'complete' || document.readyState === 'interactive') {
			initPanel();
		} else {
			setTimeout(waitForDOMReady, 500);
		}
	}

	window.addEventListener('beforeunload', () => {
		if (currentTaskTimer) clearTimeout(currentTaskTimer);
		if (isRunning) localStorage.setItem(STORAGE_KEY, 'true');
	});

	waitForDOMReady();

})();