background-keep-alive

后台保活工具集:Web Worker 定时器、音频保活、网络请求监控

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/559089/1715548/background-keep-alive.js을(를) 사용하여 포함하는 라이브러리입니다.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         background-keep-alive
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  后台保活工具集:Web Worker 定时器、音频保活、网络请求监控
// @description:en  Background Keep-Alive Toolkit: Web Worker timer, Audio keep-alive, Network request monitor
// @author       urzeye
// @match        *://*/*
// @license      MIT
// ==/UserScript==

/**
 * ============================================================================
 * BackgroundKeepAlive Toolkit
 * ============================================================================
 * 解决浏览器后台标签页节流问题的通用工具集
 *
 * 包含三大模块:
 * 1. BackgroundTimer  - 基于 Web Worker 的保活定时器
 * 2. AudioKeepAlive   - 静音音频对抗 Chrome 5分钟强力休眠
 * 3. NetworkMonitor   - Hook Fetch/XHR 监控任务完成状态
 */

// ============================================================================
// 模块1: BackgroundTimer - Web Worker 保活定时器
// ============================================================================

/**
 * BackgroundTimer - 基于 Web Worker 的保活定时器
 *
 * 解决的问题:
 * 现代浏览器对后台标签页进行资源节流(Throttling),导致 setInterval 定时器
 * 被严重降频(从正常间隔变为 1 分钟甚至更长)。Web Worker 运行在独立线程中,
 * 不受此限制。
 *
 * @example
 * const timer = new BackgroundTimer(() => {
 *     console.log('后台心跳:', new Date().toTimeString());
 * }, 1000);
 * timer.start();
 */
class BackgroundTimer {
	/**
	 * @param {Function} callback - 定时回调函数
	 * @param {number} intervalMs - 定时间隔(毫秒),默认 1000ms
	 */
	constructor(callback, intervalMs = 1000) {
		this.callback = callback;
		this.intervalMs = intervalMs;
		this.worker = null;
		this.workerUrl = null;
		this._isRunning = false;
	}

	start() {
		if (this._isRunning) return;

		// 尝试使用 Web Worker(某些页面可能有 CSP 限制)
		try {
			const workerScript = `
				let timerId = null;
				let interval = ${this.intervalMs};

				self.onmessage = function(e) {
					const { type, data } = e.data;
					switch (type) {
						case 'start':
							if (timerId) clearInterval(timerId);
							timerId = setInterval(() => {
								self.postMessage({ type: 'tick', timestamp: Date.now() });
							}, interval);
							break;
						case 'stop':
							if (timerId) { clearInterval(timerId); timerId = null; }
							break;
						case 'setInterval':
							interval = data;
							if (timerId) {
								clearInterval(timerId);
								timerId = setInterval(() => {
									self.postMessage({ type: 'tick', timestamp: Date.now() });
								}, interval);
							}
							break;
					}
				};
			`;

			const blob = new Blob([workerScript], { type: 'application/javascript' });
			this.workerUrl = URL.createObjectURL(blob);
			this.worker = new Worker(this.workerUrl);

			this.worker.onmessage = (e) => {
				if (e.data.type === 'tick' && typeof this.callback === 'function') {
					try { this.callback(e.data.timestamp); }
					catch (err) { console.error('[BackgroundTimer] Callback error:', err); }
				}
			};

			this.worker.onerror = (err) => console.error('[BackgroundTimer] Worker error:', err);
			this.worker.postMessage({ type: 'start' });
			this._useWorker = true;
		} catch (err) {
			// CSP 或其他限制导致 Worker 创建失败,回退到普通 setInterval
			console.warn('[BackgroundTimer] Worker creation failed (CSP?), falling back to setInterval:', err.message);
			this._fallbackTimerId = setInterval(() => {
				if (typeof this.callback === 'function') {
					try { this.callback(Date.now()); }
					catch (e) { console.error('[BackgroundTimer] Callback error:', e); }
				}
			}, this.intervalMs);
			this._useWorker = false;
		}

		this._isRunning = true;
	}

	stop() {
		if (!this._isRunning) return;

		if (this._useWorker) {
			// Worker 模式
			if (this.worker) {
				this.worker.postMessage({ type: 'stop' });
				this.worker.terminate();
				this.worker = null;
			}
			if (this.workerUrl) {
				URL.revokeObjectURL(this.workerUrl);
				this.workerUrl = null;
			}
		} else {
			// 回退模式
			if (this._fallbackTimerId) {
				clearInterval(this._fallbackTimerId);
				this._fallbackTimerId = null;
			}
		}

		this._isRunning = false;
	}

	setInterval(intervalMs) {
		this.intervalMs = intervalMs;
		if (!this._isRunning) return;

		if (this._useWorker && this.worker) {
			// Worker 模式:发送消息动态调整
			this.worker.postMessage({ type: 'setInterval', data: intervalMs });
		} else if (this._fallbackTimerId) {
			// 回退模式:重建定时器
			clearInterval(this._fallbackTimerId);
			this._fallbackTimerId = setInterval(() => {
				if (typeof this.callback === 'function') {
					try { this.callback(Date.now()); }
					catch (e) { console.error('[BackgroundTimer] Callback error:', e); }
				}
			}, intervalMs);
		}
	}

	isRunning() { return this._isRunning; }

	destroy() {
		this.stop();
		this.callback = null;
	}
}

// ============================================================================
// 模块2: AudioKeepAlive - 静音音频保活
// ============================================================================

/**
 * AudioKeepAlive - 使用静音音频对抗 Chrome 5分钟强力休眠
 *
 * 原理:浏览器通常不会节流正在播放音频的标签页(保证后台听歌体验)
 * 通过播放极低音量的音频,可以保持标签页活跃状态
 *
 * 注意:需要用户交互后才能启动(浏览器自动播放策略限制)
 *
 * @example
 * const audio = new AudioKeepAlive();
 * document.addEventListener('click', () => audio.start(), { once: true });
 */
class AudioKeepAlive {
	constructor() {
		this.audioCtx = null;
		this.oscillator = null;
		this.gainNode = null;
		this._isActive = false;
	}

	/**
	 * 启动音频保活
	 * @returns {boolean} 是否启动成功
	 */
	start() {
		if (this._isActive) return true;

		try {
			const AudioContext = window.AudioContext || window.webkitAudioContext;
			if (!AudioContext) {
				console.warn('[AudioKeepAlive] AudioContext not supported');
				return false;
			}

			this.audioCtx = new AudioContext();

			// 如果被挂起,尝试恢复
			if (this.audioCtx.state === 'suspended') {
				this.audioCtx.resume();
			}

			// 创建振荡器(产生音频信号)
			this.oscillator = this.audioCtx.createOscillator();
			this.oscillator.type = 'sine';
			this.oscillator.frequency.value = 1; // 极低频率,人耳几乎听不到

			// 创建增益节点(控制音量)
			this.gainNode = this.audioCtx.createGain();
			this.gainNode.gain.value = 0.0001; // 极低音量

			// 连接节点
			this.oscillator.connect(this.gainNode);
			this.gainNode.connect(this.audioCtx.destination);

			// 开始播放
			this.oscillator.start();
			this._isActive = true;

			console.log('[AudioKeepAlive] Started');
			return true;
		} catch (err) {
			console.error('[AudioKeepAlive] Start error:', err);
			return false;
		}
	}

	/**
	 * 停止音频保活
	 */
	stop() {
		if (!this._isActive) return;

		try {
			if (this.oscillator) {
				this.oscillator.stop();
				this.oscillator.disconnect();
				this.oscillator = null;
			}
			if (this.gainNode) {
				this.gainNode.disconnect();
				this.gainNode = null;
			}
			if (this.audioCtx) {
				this.audioCtx.close();
				this.audioCtx = null;
			}
		} catch (err) {
			console.error('[AudioKeepAlive] Stop error:', err);
		}

		this._isActive = false;
		console.log('[AudioKeepAlive] Stopped');
	}

	/**
	 * 获取运行状态
	 */
	isActive() { return this._isActive; }

	/**
	 * 销毁实例
	 */
	destroy() { this.stop(); }
}

// ============================================================================
// 模块3: NetworkMonitor - 网络请求监控
// ============================================================================

/**
 * NetworkMonitor - Hook Fetch/XHR 监控任务完成状态
 *
 * 核心算法:防抖 + 活跃计数器
 * 1. 记录当前活跃请求数
 * 2. 每次请求开始/结束都重置静默计时器
 * 3. 活跃数为 0 + 静默期结束 + DOM 验证通过 → 触发完成回调
 *
 * @example
 * const monitor = new NetworkMonitor({
 *     urlPatterns: ['batchexecute', 'stream/generate'],
 *     silenceThreshold: 3000,
 *     onComplete: () => console.log('任务完成'),
 *     domValidation: (ctx) => !document.querySelector('.stop-button')
 * });
 * monitor.start();
 */
class NetworkMonitor {
	/**
	 * @param {Object} options - 配置选项
	 * @param {string[]} options.urlPatterns - 要监控的 URL 模式(包含匹配)
	 * @param {number} options.silenceThreshold - 静默判定时间(毫秒),默认 3000
	 * @param {Function} options.onComplete - 任务完成回调
	 * @param {Function} [options.onStart] - 任务开始回调(可选)
	 * @param {Function} [options.domValidation] - DOM 二次验证函数(可选),返回 true 表示验证通过
	 */
	constructor(options = {}) {
		this.urlPatterns = options.urlPatterns || [];
		this.silenceThreshold = options.silenceThreshold || 3000;
		this.onComplete = options.onComplete || (() => { });
		this.onStart = options.onStart || null;
		this.domValidation = options.domValidation || null;

		this._activeCount = 0;
		this._silenceTimer = null;
		this._isMonitoring = false;
		this._originalFetch = null;
		this._originalXhrOpen = null;
		this._originalXhrSend = null;
		this._lastUrl = '';
		this._hasTriggeredStart = false;

		// 绑定方法
		this._hookedFetch = this._hookedFetch.bind(this);
	}

	/**
	 * 开始监控
	 */
	start() {
		if (this._isMonitoring) return;

		// Hook Fetch
		this._originalFetch = window.fetch;
		window.fetch = this._hookedFetch;

		// Hook XHR
		this._hookXHR();

		this._isMonitoring = true;
		console.log('[NetworkMonitor] Started monitoring (Fetch + XHR)');
	}

	/**
	 * 停止监控
	 */
	stop() {
		if (!this._isMonitoring) return;

		// 恢复原始 Fetch
		if (this._originalFetch) {
			window.fetch = this._originalFetch;
			this._originalFetch = null;
		}

		// 恢复原始 XHR
		this._unhookXHR();

		// 清理定时器
		if (this._silenceTimer) {
			clearTimeout(this._silenceTimer);
			this._silenceTimer = null;
		}

		this._isMonitoring = false;
		this._activeCount = 0;
		this._hasTriggeredStart = false;
		console.log('[NetworkMonitor] Stopped monitoring');
	}

	/**
	 * 获取当前是否空闲
	 */
	isIdle() { return this._activeCount === 0; }

	/**
	 * 获取当前活跃请求数
	 */
	getActiveCount() { return this._activeCount; }

	/**
	 * 销毁实例
	 */
	destroy() {
		this.stop();
		this.onComplete = null;
		this.onStart = null;
		this.domValidation = null;
	}

	/**
	 * 检查 URL 是否匹配监控模式
	 * @private
	 */
	_isTargetUrl(url) {
		if (!url || this.urlPatterns.length === 0) return false;
		return this.urlPatterns.some(pattern => url.includes(pattern));
	}

	/**
	 * 尝试触发完成回调
	 * @private
	 */
	_tryTriggerComplete() {
		if (this._activeCount > 0) return;

		// 构建上下文信息
		const ctx = {
			activeCount: this._activeCount,
			lastUrl: this._lastUrl,
			timestamp: Date.now()
		};

		// DOM 二次验证
		if (typeof this.domValidation === 'function') {
			try {
				const isValid = this.domValidation(ctx);
				if (!isValid) {
					// 验证未通过,延迟重试
					console.log('[NetworkMonitor] DOM validation failed, retrying...');
					this._silenceTimer = setTimeout(() => this._tryTriggerComplete(), 1000);
					return;
				}
			} catch (err) {
				console.error('[NetworkMonitor] DOM validation error:', err);
			}
		}

		// 触发完成回调
		console.log('[NetworkMonitor] Task complete');
		this._hasTriggeredStart = false;
		try {
			this.onComplete(ctx);
		} catch (err) {
			console.error('[NetworkMonitor] onComplete error:', err);
		}
	}

	/**
	 * Hook 后的 fetch 函数
	 * @private
	 */
	async _hookedFetch(...args) {
		const url = args[0] ? args[0].toString() : '';

		// 非目标请求,直接透传
		if (!this._isTargetUrl(url)) {
			return this._originalFetch(...args);
		}

		// 目标请求:开始计数
		this._activeCount++;
		this._lastUrl = url;

		// 清除静默计时器
		if (this._silenceTimer) {
			clearTimeout(this._silenceTimer);
			this._silenceTimer = null;
		}

		// 触发开始回调(仅首次)
		if (!this._hasTriggeredStart && typeof this.onStart === 'function') {
			this._hasTriggeredStart = true;
			try { this.onStart({ url, timestamp: Date.now() }); }
			catch (err) { console.error('[NetworkMonitor] onStart error:', err); }
		}

		try {
			const response = await this._originalFetch(...args);
			const clone = response.clone();

			// 在后台读取流
			this._readStream(clone).catch(() => { });

			return response;
		} catch (error) {
			// fetch 失败也要减计数
			this._decrementAndSchedule();
			throw error;
		}
	}

	/**
	 * 读取响应流
	 * @private
	 */
	async _readStream(response) {
		try {
			const reader = response.body.getReader();
			while (true) {
				const { done } = await reader.read();
				if (done) break;
			}
		} catch (err) {
			// 流读取错误,忽略
		} finally {
			this._decrementAndSchedule();
		}
	}

	/**
	 * 减少计数并调度静默计时器
	 * @private
	 */
	_decrementAndSchedule() {
		this._activeCount = Math.max(0, this._activeCount - 1);

		// 清除并重新设置静默计时器
		if (this._silenceTimer) {
			clearTimeout(this._silenceTimer);
		}
		this._silenceTimer = setTimeout(() => this._tryTriggerComplete(), this.silenceThreshold);
	}

	/**
	 * Hook XMLHttpRequest
	 * @private
	 */
	_hookXHR() {
		const self = this;
		this._originalXhrOpen = XMLHttpRequest.prototype.open;
		this._originalXhrSend = XMLHttpRequest.prototype.send;

		// Hook open 方法,记录 URL
		XMLHttpRequest.prototype.open = function (method, url, ...rest) {
			this._networkMonitorUrl = url ? url.toString() : '';
			return self._originalXhrOpen.call(this, method, url, ...rest);
		};

		// Hook send 方法,监控请求生命周期
		XMLHttpRequest.prototype.send = function (body) {
			const url = this._networkMonitorUrl || '';

			// 非目标请求,直接透传
			if (!self._isTargetUrl(url)) {
				return self._originalXhrSend.call(this, body);
			}

			// 目标请求:开始计数
			self._activeCount++;
			self._lastUrl = url;

			// 清除静默计时器
			if (self._silenceTimer) {
				clearTimeout(self._silenceTimer);
				self._silenceTimer = null;
			}

			// 触发开始回调(仅首次)
			if (!self._hasTriggeredStart && typeof self.onStart === 'function') {
				self._hasTriggeredStart = true;
				try { self.onStart({ url, timestamp: Date.now(), type: 'xhr' }); }
				catch (err) { console.error('[NetworkMonitor] onStart error:', err); }
			}

			// 监听请求完成
			const onComplete = () => {
				self._decrementAndSchedule();
			};

			this.addEventListener('load', onComplete);
			this.addEventListener('error', onComplete);
			this.addEventListener('abort', onComplete);
			this.addEventListener('timeout', onComplete);

			return self._originalXhrSend.call(this, body);
		};
	}

	/**
	 * 恢复原始 XMLHttpRequest
	 * @private
	 */
	_unhookXHR() {
		if (this._originalXhrOpen) {
			XMLHttpRequest.prototype.open = this._originalXhrOpen;
			this._originalXhrOpen = null;
		}
		if (this._originalXhrSend) {
			XMLHttpRequest.prototype.send = this._originalXhrSend;
			this._originalXhrSend = null;
		}
	}
}

// ============================================================================
// 导出到全局
// ============================================================================

if (typeof window !== 'undefined') {
	window.BackgroundTimer = BackgroundTimer;
	window.AudioKeepAlive = AudioKeepAlive;
	window.NetworkMonitor = NetworkMonitor;
}