diff --git a/CLAUDE.md b/CLAUDE.md
index 8ba44d69..ea28f167 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2795,6 +2795,78 @@ if (cachedResponse) {
- 在返回前验证 `blob.size > 0` 可以自动修复历史问题
- 删除无效缓存后重新获取,确保用户看到正确的内容
+### CDN 响应必须多重验证后才能缓存
+
+**场景**: Service Worker 从 CDN 获取静态资源并缓存时
+
+❌ **错误示例**:
+```typescript
+// 错误:只检查 response.ok,可能缓存 CDN 返回的 HTML 错误页面
+const response = await fetch(cdnUrl);
+if (response.ok) {
+ cache.put(request, response.clone());
+ return response; // 可能是 404 页面被当作 JS 执行!
+}
+```
+
+✅ **正确示例**:
+```typescript
+const response = await fetch(cdnUrl);
+if (response.ok) {
+ // 1. Content-Type 验证
+ const contentType = response.headers.get('Content-Type') || '';
+ const isValidType = contentType.includes('javascript') ||
+ contentType.includes('css') ||
+ contentType.includes('json');
+ if (!isValidType) continue; // 尝试下一个源
+
+ // 2. Content-Length 验证(排除空响应)
+ const length = parseInt(response.headers.get('Content-Length') || '0', 10);
+ if (length > 0 && length < 50) continue;
+
+ // 3. 内容采样验证(检测 HTML 错误页面)
+ const sample = await response.clone().text().then(t => t.slice(0, 200));
+ if (sample.includes('
-
+
diff --git a/apps/web/public/version.json b/apps/web/public/version.json
index db908c78..f25d2cf6 100644
--- a/apps/web/public/version.json
+++ b/apps/web/public/version.json
@@ -1,6 +1,6 @@
{
"version": "0.5.25",
- "buildTime": "2026-01-20T05:11:17.776Z",
+ "buildTime": "2026-01-20T05:11:18.087Z",
"gitCommit": "unknown",
"changelog": []
}
\ No newline at end of file
diff --git a/apps/web/src/sw/cdn-fallback.ts b/apps/web/src/sw/cdn-fallback.ts
index 668b6fe2..a3fd0610 100644
--- a/apps/web/src/sw/cdn-fallback.ts
+++ b/apps/web/src/sw/cdn-fallback.ts
@@ -234,9 +234,13 @@ export async function fetchFromCDNWithFallback(
const response = await fetchWithTimeout(url);
if (response.ok) {
- // 验证响应类型,防止把 404 页面当作 JS 执行
+ // ============================================
+ // 多重验证:确保 CDN 返回的是有效资源
+ // ============================================
+
+ // 1. Content-Type 验证
const contentType = response.headers.get('Content-Type') || '';
- const isValidResponse =
+ const isValidContentType =
contentType.includes('javascript') ||
contentType.includes('css') ||
contentType.includes('json') ||
@@ -245,10 +249,38 @@ export async function fetchFromCDNWithFallback(
contentType.includes('woff') ||
contentType.includes('application/octet-stream');
- if (!isValidResponse) {
- console.warn(`[CDN Fallback] ${cdn.name} returned invalid Content-Type: ${contentType}`);
+ if (!isValidContentType) {
+ console.warn(`[CDN Fallback] ${cdn.name} invalid Content-Type: ${contentType}`);
markCDNFailure(cdn.name);
- continue; // 尝试下一个 CDN
+ continue;
+ }
+
+ // 2. Content-Length 验证(JS/CSS 文件不应该太小)
+ const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10);
+ const isTextResource = contentType.includes('javascript') || contentType.includes('css') || contentType.includes('json');
+ if (isTextResource && contentLength > 0 && contentLength < 50) {
+ console.warn(`[CDN Fallback] ${cdn.name} response too small: ${contentLength} bytes`);
+ markCDNFailure(cdn.name);
+ continue;
+ }
+
+ // 3. 内容采样验证(检测 HTML 错误页面)
+ const clonedResponse = response.clone();
+ try {
+ const textSample = await clonedResponse.text().then(t => t.slice(0, 200));
+ const looksLikeHtml = textSample.includes(' {
// Listen for custom event from main.tsx
const handleUpdateAvailable = async (event: Event) => {
const customEvent = event as CustomEvent;
- // console.log('[VersionUpdatePrompt] Update available:', customEvent.detail);
- const version = customEvent.detail?.version;
+ const newVersion = customEvent.detail?.version;
+
+ // 获取当前运行的版本(从 HTML meta 标签)
+ const currentVersionMeta = document.querySelector('meta[name="app-version"]');
+ const currentVersion = currentVersionMeta?.getAttribute('content');
try {
// Fetch detailed version info (changelog)
const res = await fetch(`/version.json?t=${Date.now()}`);
if (res.ok) {
const data = await res.json();
+
+ // 如果当前版本已经是最新版本,不显示更新提示
+ if (currentVersion && data.version === currentVersion) {
+ // console.log('[VersionUpdatePrompt] Already on latest version, skipping prompt');
+ return;
+ }
+
// Use fetched data if versions match or if event didn't specify version
- if (!version || data.version === version) {
+ if (!newVersion || data.version === newVersion) {
setUpdateAvailable(data);
return;
}
@@ -35,6 +45,11 @@ export const VersionUpdatePrompt: React.FC = () => {
console.warn('Failed to fetch version.json:', error);
}
+ // 如果无法获取 version.json,但事件带了版本号,检查是否相同
+ if (currentVersion && newVersion && currentVersion === newVersion) {
+ return; // 已经是最新版本
+ }
+
// Fallback to event detail
setUpdateAvailable(customEvent.detail);
};