/* AO3 Mirror Service Worker v5 — Client-side cache + offline fallback + navigation intercept * * Deployed at /sw-YYYYMMDD.js (versioned), /sw.js redirects to latest. * Server injects into HTML pages. */ 'use strict'; const MIRROR_DOMAINS = ['agento3.miscs.dev']; const PRIMARY_DOMAIN = 'agento3.miscs.dev'; // Static asset types to cache aggressively const STATIC_EXTENSIONS = [ '.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.svg', '.webp', '.json', ]; // Cache names const STATIC_CACHE = 'ao3-static-v1'; const HTML_CACHE = 'ao3-html-v1'; // Navigation timeout (25s, like go3) const NAV_FETCH_TIMEOUT_MS = 25000; // ─── Helpers ────────────────────────────────────────────────────────── function primaryMirrorHost() { try { var scopeHost = new URL(self.registration.scope).hostname; if (scopeHost && scopeHost.indexOf('.') !== -1) return scopeHost; } catch (e) {} for (var i = 0; i < MIRROR_DOMAINS.length; i++) { var h = MIRROR_DOMAINS[i]; if (h && h.indexOf('.') !== -1) return h; } return PRIMARY_DOMAIN; } function isStaticAsset(url) { var path = url.pathname.toLowerCase(); for (var i = 0; i < STATIC_EXTENSIONS.length; i++) { if (path.endsWith(STATIC_EXTENSIONS[i])) return true; } return false; } function hostFromRequest(request) { try { return new URL(request.url).hostname || ''; } catch (e) { return ''; } } function repairNavigationURL(url) { var host = url.hostname; if (!host || host.indexOf('.') !== -1 || host === 'localhost') return url; var mirror = primaryMirrorHost(); if (!mirror) return url; var fixed = new URL(url.toString()); fixed.hostname = mirror; fixed.pathname = '/' + host + (fixed.pathname || '/'); return fixed; } // ─── Offline / mirror picker page ───────────────────────────────────── function buildMirrorPickerPage(currentHost, mirrors) { var mirrorItems = mirrors .filter(function (h) { return h; }) .map(function (h) { return '
  • ' + h + '
  • '; }) .join(''); return ( '无法访问镜像站点' + '
    ' + '

    无法连接到镜像站点

    ' + '

    浏览器未能与 ' + currentHost + ' 建立网络连接(例如断网、DNS 失败或被防火墙拦截)。请按下面顺序逐步排查。

    ' + '
      ' + '
    1. 1先检查网络连接

      ' + '

      确认设备已联网:可尝试打开其他网站或 App。若使用 Wi‑Fi,请检查路由器是否正常。

    2. ' + '
    3. 2尝试切换到移动流量

      ' + '

      部分宽带或校园网可能对镜像域名有限制。请关闭 Wi‑Fi,使用手机 4G / 5G 流量 重新访问。

    4. ' + '
    5. 3尝试备用域名

      ' + '

      点击下方备用镜像站点:

      ' + '
        ' + mirrorItems + '
    6. ' + '
    ' + '

    页面由 AO3 Mirror Service Worker 提供。若以上步骤后仍无法打开,请稍后再试。

    ' + '
    ' ); } function offlineGuideResponse(currentHost) { return new Response(buildMirrorPickerPage(currentHost, MIRROR_DOMAINS), { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', }, }); } // ─── Network strategies ─────────────────────────────────────────────── function handleNavigation(request) { var currentHost = hostFromRequest(request); var url = new URL(request.url); // Repair malformed navigation URLs var navUrl = repairNavigationURL(url); var fetchInit = {}; if (navUrl.href !== request.url) { fetchInit = { method: request.method, headers: request.headers, credentials: request.credentials, redirect: 'follow', referrer: request.referrer, referrerPolicy: request.referrerPolicy, }; } var ctrl = new AbortController(); var timer = setTimeout(function () { ctrl.abort(); }, NAV_FETCH_TIMEOUT_MS); var fetchTarget = navUrl.href !== request.url ? new Request(navUrl.href, fetchInit) : request; return fetch(fetchTarget, { signal: ctrl.signal }) .finally(function () { clearTimeout(timer); }) .then(function (response) { if (response.redirected && response.url) { return Response.redirect(response.url, 302); } return response; }) .catch(function () { return offlineGuideResponse(currentHost); }); } function handleStatic(request) { // Cache-first for static assets return caches.open(STATIC_CACHE).then(function (cache) { return cache.match(request).then(function (cached) { if (cached) { // Background revalidation fetch(request).then(function (response) { if (response && response.ok) { cache.put(request, response); } }).catch(function () {}); return cached; } // Network with cache fallback return fetch(request).then(function (response) { if (response && response.ok) { var cloned = response.clone(); cache.put(request, cloned); } return response; }).catch(function () { // Offline — return whatever we have return cache.match(request); }); }); }); } // ─── Install / Activate ─────────────────────────────────────────────── self.addEventListener('install', function (event) { self.skipWaiting(); }); self.addEventListener('activate', function (event) { event.waitUntil(self.clients.claim()); // Clean old caches event.waitUntil( caches.keys().then(function (keys) { return Promise.all( keys.map(function (key) { if (key !== STATIC_CACHE && key !== HTML_CACHE) { return caches.delete(key); } }) ); }) ); }); // ─── Fetch handler ──────────────────────────────────────────────────── self.addEventListener('fetch', function (event) { if (event.request.method !== 'GET') return; var url = new URL(event.request.url); // Don't intercept SW or monitor paths if (url.pathname.startsWith('/sw') || url.pathname === '/mirror-domains.json') return; if (url.pathname === '/_monitor' || url.pathname.startsWith('/_monitor/')) return; if (url.pathname === '/stats' || url.pathname === '/metrics' || url.pathname === '/health') return; // Static assets: cache-first if (isStaticAsset(url)) { event.respondWith(handleStatic(event.request)); return; } // Navigation (HTML pages): network-first with offline fallback if (event.request.mode === 'navigate' || event.request.destination === 'document') { event.respondWith( handleNavigation(event.request).catch(function () { return offlineGuideResponse(hostFromRequest(event.request)); }) ); } // Other requests pass through to server normally });