const CACHE = 'music-box-v5'; const AUDIO_CACHE = 'zen-audio-cache'; function isAudioRequest(request) { return request.url.includes('.mp3') || request.url.includes('oss'); } async function buildPartialAudioResponse(request, cachedResponse) { const rangeHeader = request.headers.get('range'); if (!rangeHeader) return cachedResponse; const match = /bytes=(\d*)-(\d*)/.exec(rangeHeader); if (!match) return cachedResponse; const blob = await cachedResponse.blob(); const size = blob.size; let start = match[1] ? Number(match[1]) : 0; let end = match[2] ? Number(match[2]) : size - 1; if (!Number.isFinite(start) || start < 0) start = 0; if (!Number.isFinite(end) || end >= size) end = size - 1; if (start >= size || start > end) { return new Response(null, { status: 416, headers: { 'Accept-Ranges': 'bytes', 'Content-Range': `bytes */${size}` } }); } const partialBlob = blob.slice(start, end + 1, blob.type || cachedResponse.headers.get('content-type') || 'audio/mpeg'); return new Response(partialBlob, { status: 206, headers: { 'Accept-Ranges': 'bytes', 'Content-Length': String(end - start + 1), 'Content-Range': `bytes ${start}-${end}/${size}`, 'Content-Type': partialBlob.type || cachedResponse.headers.get('content-type') || 'audio/mpeg' } }); } async function handleAudioRequest(request) { const audioCache = await caches.open(AUDIO_CACHE); const cached = await audioCache.match(request.url); if (cached) { return buildPartialAudioResponse(request, cached); } return fetch(request); } self.addEventListener('install', e => { self.skipWaiting(); e.waitUntil( caches.open(CACHE).then(c => c.addAll([ './', './index.html', './zen_box.js', './zen_box.css' ])) ); }); self.addEventListener('activate', e => { e.waitUntil( caches.keys().then(keys => Promise.all( keys.filter(k => k !== CACHE && k !== AUDIO_CACHE).map(k => caches.delete(k)) ) ).then(() => clients.claim()) ); }); self.addEventListener('fetch', e => { // 音频文件:优先命中 zen-audio-cache(离线播放必需),否则走网络 if (isAudioRequest(e.request)) { e.respondWith( handleAudioRequest(e.request).catch(() => fetch(e.request)) ); return; } // playlist.json 使用 Network First if (e.request.url.includes('playlist.json')) { e.respondWith( fetch(e.request) .then(networkResponse => { return caches.open(CACHE).then(cache => { cache.put(e.request, networkResponse.clone()); return networkResponse; }); }) .catch(() => { return caches.match(e.request); }) ); return; } // 其他文件(App Shell)使用 Cache First e.respondWith( caches.match(e.request).then(r => r || fetch(e.request)) ); });