您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
使用内存缓存优化视频缓存,特别针对频繁切换视频场景进行优化,提升播放流畅度。集成WeakMap防泄漏、设备与网络自适应、主动清理等健壮性增强功能。
// ==UserScript== // @name MediaPlay 缓存优化 (增强版) // @namespace http://tampermonkey.net/ // @version 2.2.4 // @description 使用内存缓存优化视频缓存,特别针对频繁切换视频场景进行优化,提升播放流畅度。集成WeakMap防泄漏、设备与网络自适应、主动清理等健壮性增强功能。 // @author KiwiFruit // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_registerMenuCommand // @connect self // @connect cdn.jsdelivr.net // @connect unpkg.com // @license MIT // ==/UserScript== (function () { 'use strict'; /* global tf */ // ===================== 配置参数 ===================== const DEBUG_MODE = false; // 【核心开关】设为 false 时,用户环境不输出 info 日志;设为 true 时,开发者环境输出所有日志。 const CACHE_NAME = 'video-cache-v1'; const MAX_CACHE_ENTRIES = 30; // 正常场景缓存容量 const MIN_SEGMENT_SIZE_MB = 0.5; const MAX_CACHE_AGE_MS = 5 * 60 * 1000; // 5分钟 const RL_TRAINING_INTERVAL = 60 * 1000; // 每60秒训练一次模型 // 新增:场景感知配置 const SCENE_CONFIG = { isFrequentSwitching: false, // 是否频繁切换视频场景 switchCacheSize: 15, // 切换场景时的缓存容量 switchRlTrainingInterval: 120000 // 切换场景时的训练间隔 }; // 新增:设备与网络自适应配置 const DEVICE_CONFIG = { isLowEnd: navigator.deviceMemory < 4 || navigator.hardwareConcurrency < 4 }; // 新增:根据网络类型动态设置的预加载时长(秒) const NETWORK_BUFFER_CONFIG = { '2g': 15, '3g': 25, '4g': 40, 'slow-2g': 10, 'fast-3g': 30, 'lte': 40, '5g': 40, 'unknown': 25 // 默认值 }; // ===================== 全局缓存 ===================== let NETWORK_SPEED_CACHE = { value: 15, timestamp: 0 }; // 网络测速结果缓存 const PROTOCOL_PARSE_CACHE = new Map(); // 协议解析结果缓存 // 新增:使用 WeakMap 存储活跃的 VideoCacheManager 实例,防止内存泄漏 const ACTIVE_MANAGERS = new WeakMap(); // 新增:定期清理任务ID let cleanupIntervalId = null; // ===================== 工具函数 ===================== function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function log(...args) { if (DEBUG_MODE) { console.log('[SmartVideoCache]', ...args); } } function warn(...args) { // 警告信息在生产环境也应保留,以便用户发现问题 console.warn('[SmartVideoCache]', ...args); } function error(...args) { // 错误信息在生产环境必须保留,至关重要 console.error('[SmartVideoCache]', ...args); } function blobToUint8Array(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(new Uint8Array(reader.result)); reader.onerror = reject; reader.readAsArrayBuffer(blob); }); } // 新增:节流函数,用于优化高频事件 function throttle(func, wait) { let timeout = null; return function (...args) { if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(this, args); }, wait); } }; } // ===================== 网络测速(优化版:添加缓存)===================== async function getNetworkSpeed() { const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 const now = Date.now(); // 检查缓存 if (now - NETWORK_SPEED_CACHE.timestamp < CACHE_DURATION) { log('使用缓存测速结果: ' + NETWORK_SPEED_CACHE.value + ' Mbps'); return NETWORK_SPEED_CACHE.value; } try { if (navigator.connection && navigator.connection.downlink) { const speed = navigator.connection.downlink; // Mbps NETWORK_SPEED_CACHE = { value: speed, timestamp: now }; log('navigator.connection测速结果: ' + speed + ' Mbps'); return speed; } } catch (e) { warn('navigator.connection.downlink 不可用'); } const testUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/test-1mb.bin'; const startTime = performance.now(); try { const res = await fetch(testUrl, { cache: 'no-store' }); const blob = await res.blob(); const duration = (performance.now() - startTime) / 1000; const speedMbps = (8 * blob.size) / (1024 * 1024 * duration); // MB/s -> Mbps NETWORK_SPEED_CACHE = { value: speedMbps, timestamp: now }; log('主动测速结果: ' + speedMbps.toFixed(2) + ' Mbps'); return speedMbps; } catch (err) { warn('主动测速失败,使用默认值 15 Mbps'); return 15; } } // 新增:检测网络状况 profile function detectNetworkProfile() { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (connection && connection.effectiveType) { return connection.effectiveType.toLowerCase(); } return 'unknown'; } // 新增:获取自适应的预加载时长 function getAdaptiveBufferDuration() { const networkType = detectNetworkProfile(); const bufferDuration = NETWORK_BUFFER_CONFIG[networkType] || NETWORK_BUFFER_CONFIG.unknown; log('检测到网络类型: ' + networkType + ', 使用自适应缓冲时长: ' + bufferDuration + 's'); return bufferDuration; } // ===================== MIME 类型映射 ===================== const MIME_TYPE_MAP = { h264: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', h265: 'video/mp4; codecs="hvc1.1.L93.B0"', av1: 'video/webm; codecs="av01.0.08M.10"', vp9: 'video/webm; codecs="vp9"', flv: 'video/flv' }; // ===================== 协议检测器 ===================== class ProtocolDetector { static detect(url, content) { if (url.endsWith('.m3u8') || content.includes('#EXTM3U')) return 'hls'; if (url.endsWith('.mpd') || content.includes('<MPD')) return 'dash'; if (url.endsWith('.webm') || content.includes('webm')) return 'webm'; if (url.endsWith('.flv') || content.includes('FLV')) return 'flv'; if (url.endsWith('.mp4') || url.includes('.m4s')) return 'mp4-segmented'; return 'unknown'; } } // ===================== 协议解析器接口(优化版:添加缓存)===================== class ProtocolParser { static parse(url, content, mimeType) { // 检查解析缓存(有效期5分钟) const cached = PROTOCOL_PARSE_CACHE.get(url); if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { log('命中协议解析缓存: ' + url); return cached.result; } const protocol = ProtocolDetector.detect(url, content); let result; switch (protocol) { case 'hls': result = HLSParser.parse(url, content, mimeType); break; case 'dash': result = DASHParser.parse(url, content, mimeType); break; case 'mp4-segmented': result = MP4SegmentParser.parse(url, content, mimeType); break; case 'flv': result = FLVParser.parse(url, content, mimeType); break; case 'webm': result = WebMParser.parse(url, content, mimeType); break; default: throw new Error('Unsupported protocol: ' + protocol); } // 缓存解析结果 PROTOCOL_PARSE_CACHE.set(url, { result, timestamp: Date.now() }); // 定时清理过期缓存 setTimeout(() => { if (Date.now() - PROTOCOL_PARSE_CACHE.get(url)?.timestamp > 5 * 60 * 1000) { PROTOCOL_PARSE_CACHE.delete(url); } }, 5 * 60 * 1000); return result; } } // ===================== HLS 解析器 ===================== class HLSParser { static parse(url, content, mimeType) { const segments = []; const lines = content.split('\n').filter(line => line.trim()); let currentSegment = {}, seq = 0; for (const line of lines) { if (line.startsWith('#EXT-X-STREAM-INF:')) { const match = line.match(/CODECS="([^"]+)"/); const codecs = match ? match[1].split(',') : ['avc1.42E01E']; currentSegment.codecs = codecs; } else if (!line.startsWith('#') && !line.startsWith('http')) { const segmentUrl = new URL(line, url).href; currentSegment.url = segmentUrl; currentSegment.seq = seq++; segments.push(currentSegment); currentSegment = {}; } } return { protocol: 'hls', segments, mimeType: mimeType || this.getMimeTypeFromCodecs(segments[0]?.codecs) }; } static getMimeTypeFromCodecs(codecs = []) { if (codecs.some(c => c.startsWith('avc1'))) return MIME_TYPE_MAP.h264; if (codecs.some(c => c.startsWith('hvc1'))) return MIME_TYPE_MAP.h265; if (codecs.some(c => c.startsWith('vp09'))) return MIME_TYPE_MAP.vp9; if (codecs.some(c => c.startsWith('av01'))) return MIME_TYPE_MAP.av1; return MIME_TYPE_MAP.h264; } } // ===================== DASH 解析器 ===================== class DASHParser { static parse(url, content, mimeType) { const parser = new DOMParser(); const xml = parser.parseFromString(content, 'application/xml'); const representations = xml.querySelectorAll('Representation'); const segments = []; let seq = 0; for (let rep of representations) { const codec = rep.getAttribute('codecs') || 'avc1.42E01E'; const base = rep.querySelector('BaseURL')?.textContent; const segmentList = rep.querySelector('SegmentList'); if (!segmentList) continue; const segmentUrls = segmentList.querySelectorAll('SegmentURL'); for (let seg of segmentUrls) { const media = seg.getAttribute('media'); if (media) { segments.push({ url: new URL(media, url).href, seq: seq++, duration: 4, codecs: [codec] }); } } } return { protocol: 'dash', segments, mimeType: mimeType || this.getMimeTypeFromCodecs(segments[0]?.codecs) }; } static getMimeTypeFromCodecs(codecs = []) { if (codecs.some(c => c.startsWith('avc1'))) return MIME_TYPE_MAP.h264; if (codecs.some(c => c.startsWith('hvc1'))) return MIME_TYPE_MAP.h265; if (codecs.some(c => c.startsWith('vp09'))) return MIME_TYPE_MAP.vp9; if (codecs.some(c => c.startsWith('av01'))) return MIME_TYPE_MAP.av1; return MIME_TYPE_MAP.h264; } } // ===================== MP4 分段解析器 ===================== class MP4SegmentParser { static parse(url, content, mimeType) { const segments = []; for (let i = 0; i < 100; i++) { segments.push({ url: url + '?segment=' + i, seq: i, duration: 4 }); } return { protocol: 'mp4-segmented', segments, mimeType: mimeType || MIME_TYPE_MAP.h264 }; } } // ===================== FLV 解析器 ===================== class FLVParser { static parse(url, content, mimeType) { return { protocol: 'flv', segments: [{ url, seq: 0, duration: 100 }], mimeType: mimeType || MIME_TYPE_MAP.flv }; } } // ===================== WebM 解析器 ===================== class WebMParser { static parse(url, content, mimeType) { return { protocol: 'webm', segments: [{ url, seq: 0, duration: 100 }], mimeType: mimeType || MIME_TYPE_MAP.vp9 }; } } // ===================== 缓存管理器(优化版:频繁切换场景专用)===================== class CacheManager { constructor() { this.cacheMap = new Map(); // url -> { blob, timestamp } } // 检查缓存是否存在 async has(url) { return this.cacheMap.has(url); } // 获取缓存 async get(url) { const entry = this.cacheMap.get(url); if (!entry) return null; if (Date.now() - entry.timestamp > MAX_CACHE_AGE_MS) { this.cacheMap.delete(url); return null; } return entry.blob; } // 存储缓存 async put(url, blob) { this.cacheMap.set(url, { blob, timestamp: Date.now() }); this.limitCacheSize(); } // 限制缓存大小(优化:支持场景感知) limitCacheSize(isVideoSwitching = false) { const limit = SCENE_CONFIG.isFrequentSwitching ? SCENE_CONFIG.switchCacheSize : MAX_CACHE_ENTRIES; if (this.cacheMap.size > limit) { const entries = Array.from(this.cacheMap.entries()); // 视频切换时优先删除最旧的缓存 if (isVideoSwitching) { entries.sort((a, b) => a[1].timestamp - b[1].timestamp); } else { // 正常使用LRU策略 entries.sort((a, b) => b[1].timestamp - a[1].timestamp); } for (let i = 0; i < this.cacheMap.size - limit; i++) { this.cacheMap.delete(entries[i][0]); } } } // 清理过期缓存 clearOldCache() { const now = Date.now(); for (const [url, entry] of this.cacheMap.entries()) { if (now - entry.timestamp > MAX_CACHE_AGE_MS) { this.cacheMap.delete(url); } } } // 新增:快速清理模式(用于视频切换) fastClear() { this.cacheMap = new Map(); log('缓存已快速清空'); } // 新增:清空所有缓存(用于视频切换) clearAll() { this.cacheMap.clear(); log('缓存已全部清空'); } } // ===================== 强化学习策略引擎(优化版:快速重置)===================== class RLStrategyEngine { constructor() { this.state = { speed: 15, pauseCount: 0, stallCount: 0 }; this.history = []; this.model = this.buildModel(); this.isTraining = false; // 训练中标记 } buildModel() { // 输入:网络速度、暂停次数、卡顿次数 // 输出:是否预加载该分片的概率 const model = tf.sequential(); model.add(tf.layers.dense({ units: 16, activation: 'relu', inputShape: [3] })); model.add(tf.layers.dense({ units: 1, activation: 'sigmoid' })); model.compile({ optimizer: 'adam', loss: 'binaryCrossentropy' }); return model; } async predictLoadDecision(speed, pauseCount, stallCount) { const input = tf.tensor2d([[speed, pauseCount, stallCount]]); const prediction = this.model.predict(input); return prediction.dataSync()[0] > 0.5; // 返回是否加载 } async train(data) { if (this.isTraining) return; // 避免并发训练 this.isTraining = true; try { // 优化:仅在数据量足够时训练(≥5条数据) const trainingInterval = SCENE_CONFIG.isFrequentSwitching ? SCENE_CONFIG.switchRlTrainingInterval : RL_TRAINING_INTERVAL; if (data.length >= 5 && (Date.now() - this.lastTrainingTime > trainingInterval)) { const xs = tf.tensor2d(data.map(d => [d.speed, d.pauseCount, d.stallCount])); const ys = tf.tensor2d(data.map(d => [d.didStall ? 0 : 1])); await this.model.fit(xs, ys, { epochs: 5 }); // 减少迭代次数 this.lastTrainingTime = Date.now(); } } catch (err) { error('RL模型训练失败:', err); } finally { this.isTraining = false; } } // 新增:重置时释放TensorFlow资源 reset() { // 释放旧模型资源 if (this.model) { this.model.dispose(); } this.state = { speed: 15, pauseCount: 0, stallCount: 0 }; this.history = []; this.model = this.buildModel(); this.lastTrainingTime = 0; log('RL策略引擎已完全重置,释放模型资源'); } // 新增:视频切换时的快速决策模式 fastDecision(segment) { // 视频切换初期使用快速决策,减少预加载 if (SCENE_CONFIG.isFrequentSwitching) { return this.state.speed > 5; // 仅在网速>5Mbps时预加载 } return this.predictLoadDecision(this.state.speed, this.state.pauseCount, this.state.stallCount); } updateState(speed, pauseCount, stallCount) { this.state = { speed, pauseCount, stallCount }; } getDecision(segment) { return SCENE_CONFIG.isFrequentSwitching ? this.fastDecision(segment) : this.predictLoadDecision(this.state.speed, this.state.pauseCount, this.state.stallCount); } } // ===================== 视频缓存管理器(优化版:增强资源释放)===================== class VideoCacheManager { constructor(videoElement) { this.video = videoElement; this.mediaSource = new MediaSource(); this.video.src = URL.createObjectURL(this.mediaSource); this.sourceBuffer = null; this.segments = []; this.cacheMap = new Map(); // seq -> blob this.pendingRequests = new Set(); this.isInitialized = false; this.cacheManager = new CacheManager(); this.rlEngine = new RLStrategyEngine(); this.abortController = new AbortController(); // 用于取消请求 this.prefetchLoopId = null; // 存储预加载循环ID this.lastActiveTime = Date.now(); // 记录最后活跃时间 // 新增:将自己与 video 元素关联到 WeakMap ACTIVE_MANAGERS.set(this.video, this); } async initializeSourceBuffer(isNewVideo = false) { if (this.isInitialized) return; try { // 使用自适应的预加载时长 const bufferDuration = getAdaptiveBufferDuration(); const sources = this.video.querySelectorAll('source'); if (sources.length === 0) return; const source = sources[0]; const src = source.src; const response = await fetch(src); const text = await response.text(); const parsed = ProtocolParser.parse(src, text); this.segments = parsed.segments; this.mimeType = parsed.mimeType; this.sourceBuffer = this.mediaSource.addSourceBuffer(this.mimeType); this.sourceBuffer.mode = 'segments'; // 标记为新视频加载 if (isNewVideo) { log('检测到新视频加载,使用场景优化配置'); } this.startPrefetchLoop(); this.isInitialized = true; } catch (err) { console.error('初始化失败:', err); } } async startPrefetchLoop() { const prefetch = () => { if (!this.isInitialized) return; // 检查是否长时间未活跃(可能已切换视频) if (Date.now() - this.lastActiveTime > 10000) { log('长时间未活跃,暂停预加载'); return; } // 使用自适应的预加载时长 const bufferDuration = getAdaptiveBufferDuration(); const now = this.video.currentTime; const targetTime = now + bufferDuration; const targetSegments = this.segments.filter(s => s.startTime <= targetTime && s.startTime >= now); for (const seg of targetSegments) { if (!this.cacheMap.has(seg.seq) && !this.pendingRequests.has(seg.seq)) { this.pendingRequests.add(seg.seq); this.prefetchSegment(seg); } } // 优化:非紧急预加载使用空闲时段处理 this.prefetchLoopId = requestIdleCallback(prefetch, { timeout: 1000 }); }; this.prefetchLoopId = prefetch(); // 启动预加载循环 } async prefetchSegment(segment) { this.lastActiveTime = Date.now(); // 更新活跃时间 const { signal } = this.abortController; try { const networkSpeed = await getNetworkSpeed(); const segmentSizeMB = MIN_SEGMENT_SIZE_MB; const estimatedDelay = (segmentSizeMB * 8) / networkSpeed; // seconds log('预加载分片 ' + segment.seq + ',预计延迟: ' + estimatedDelay.toFixed(2) + 's'); const decision = await this.rlEngine.getDecision(segment); if (!decision) { this.pendingRequests.delete(segment.seq); return; } const cached = await this.cacheManager.get(segment.url); if (cached) { this.cacheMap.set(segment.seq, cached); this.pendingRequests.delete(segment.seq); await this.appendBufferToSourceBuffer(cached); log('命中缓存分片 ' + segment.seq); return; } const response = await fetch(segment.url, { mode: 'cors', signal }); if (!response.ok) throw new Error('HTTP ' + response.status); const blob = await response.blob(); await this.cacheManager.put(segment.url, blob); this.cacheMap.set(segment.seq, blob); this.pendingRequests.delete(segment.seq); await this.appendBufferToSourceBuffer(blob); log('成功缓存并注入分片 ' + segment.seq); } catch (err) { if (err.name !== 'AbortError') { error('缓存分片 ' + segment.seq + ' 失败:', err); } this.pendingRequests.delete(segment.seq); } } async appendBufferToSourceBuffer(blob) { if (!this.sourceBuffer || this.sourceBuffer.updating) return; try { const arrayBuffer = await blob.arrayBuffer(); this.sourceBuffer.appendBuffer(arrayBuffer); } catch (err) { error('注入分片失败:', err); } } clearOldCache() { this.cacheManager.clearOldCache(); } limitCacheSize() { this.cacheManager.limitCacheSize(); } // 增强:销毁实例并释放资源(核心优化点) async destroy(isVideoSwitching = false) { if (!this.isInitialized) return; // 1. 取消所有pending请求 this.abortController.abort(); // 取消未完成的fetch // 清空pendingRequests this.pendingRequests.clear(); // 2. 终止所有正在进行的预加载循环 if (this.prefetchLoopId) { cancelIdleCallback(this.prefetchLoopId); this.prefetchLoopId = null; } // 3. 清理缓存 if (isVideoSwitching) { this.cacheManager.fastClear(); } else { this.cacheManager.clearAll(); } this.cacheMap.clear(); // 4. 释放MediaSource资源 try { if (this.mediaSource) { if (this.sourceBuffer) { this.mediaSource.removeSourceBuffer(this.sourceBuffer); } this.mediaSource.endOfStream(); this.mediaSource = null; } } catch (err) { error('MediaSource释放异常:', err); } // 5. 从 WeakMap 中移除引用 ACTIVE_MANAGERS.delete(this.video); // 6. 解除引用 if (this.video) { delete this.video.dataset.videoCacheInitialized; this.video = null; } this.rlEngine = null; log('视频缓存管理器已彻底销毁'); } } // 新增:主动清理非活跃的管理器 function cleanupInactiveManagers() { const now = Date.now(); const inactiveThreshold = 60000; // 1分钟未活跃 // 由于 WeakMap 无法直接遍历,我们无法在此处清理。 // 实际上,WeakMap 的 key (video) 被移除时,value (manager) 会自动被回收。 // 我们在这里可以做的是检查是否有其他资源需要清理,但主要的内存泄漏风险已由 WeakMap 解决。 log('主动清理任务执行,但WeakMap自动管理内存,无需手动遍历。'); } // ===================== 视频元素检测(优化版:减少监听开销)===================== function monitorVideoElements() { let observer; let isSwitching = false; // 视频切换标记 let switchingTimer = null; // 切换计时器 // 断开旧监听 function disconnectOldObserver() { if (observer) { observer.disconnect(); observer = null; } } // 新增:视频切换时的防抖处理 function startSwitchingDebounce() { if (switchingTimer) { clearTimeout(switchingTimer); } isSwitching = true; SCENE_CONFIG.isFrequentSwitching = true; // 检测到切换,启用频繁切换模式 switchingTimer = setTimeout(() => { isSwitching = false; // 3秒内没有新切换,关闭频繁切换模式 setTimeout(() => { if (!isSwitching) { SCENE_CONFIG.isFrequentSwitching = false; log('切换活动停止,恢复正常模式'); } }, 3000); }, 300); // 300ms防抖 } observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName.toLowerCase() === 'video') { startSwitchingDebounce(); handleVideoElement(node); } else { const videos = node.querySelectorAll('video'); videos.forEach((video) => { startSwitchingDebounce(); handleVideoElement(video); }); } } } } }); observer.observe(document.body, { childList: true, subtree: true }); // 新增:启动主动清理任务 cleanupIntervalId = setInterval(cleanupInactiveManagers, 30000); } function handleVideoElement(video) { // 处理旧管理器 if (video.dataset.videoCacheInitialized) { const oldManager = ACTIVE_MANAGERS.get(video); if (oldManager) { oldManager.destroy(true); // 标记为视频切换场景 oldManager.rlEngine.reset(); } } video.dataset.videoCacheInitialized = 'true'; const sources = video.querySelectorAll('source'); if (sources.length === 0) return; for (const source of sources) { const src = source.src; if (src.endsWith('.m3u8') || src.endsWith('.mpd')) { const manager = new VideoCacheManager(video); // manager.fetchManifest(src, true); // 注意:原脚本中 VideoCacheManager 没有 fetchManifest 方法,可能是笔误 manager.initializeSourceBuffer(true); // 直接调用初始化 break; } } } // ===================== 初始化 ===================== (async () => { log('视频缓存优化脚本已加载(增强版)'); try { // 首次加载时清理历史缓存 const cacheManager = new CacheManager(); cacheManager.clearAll(); // 监控视频元素 monitorVideoElements(); // 初始检测已存在的视频 document.querySelectorAll('video').forEach(handleVideoElement); // 新增:记录低端设备 if (DEVICE_CONFIG.isLowEnd) { log('检测到低端设备,将启用降级策略。'); } } catch (err) { error('初始化失败:', err); } })(); // 新增:在脚本卸载时清理定时器 window.addEventListener('beforeunload', () => { if (cleanupIntervalId) { clearInterval(cleanupIntervalId); } }); })();