Переглянути джерело

【bug】 测试修复加载白屏问题

“陈博论” 7 місяців тому
батько
коміт
0c4724c835
3 змінених файлів з 126 додано та 39 видалено
  1. 14 0
      app.py
  2. 57 33
      static/js/chat.js
  3. 55 6
      templates/index.html

+ 14 - 0
app.py

@@ -76,6 +76,20 @@ app.register_blueprint(auth_bp)
 # 存储用户当前对话ID的字典(保留用于兼容性)
 user_current_conversations: Dict[str, int] = {}
 
+
+@app.after_request
+def add_cache_control_headers(response):
+    """
+    防止首页HTML被中间层或浏览器长期缓存,避免出现"旧HTML引用新/旧静态资源"导致白屏。
+    静态资源保持默认缓存策略,仅对HTML页面禁缓存。
+    """
+    content_type = (response.content_type or "").lower()
+    if "text/html" in content_type:
+        response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
+        response.headers["Pragma"] = "no-cache"
+        response.headers["Expires"] = "0"
+    return response
+
 def save_message_to_database(user_id: int, conversation_id: int, role: str, content: str, 
                             thinking_content: str = None, message_order: int = 1) -> Message:
     """保存消息到数据库"""

+ 57 - 33
static/js/chat.js

@@ -1972,49 +1972,58 @@ document.addEventListener('DOMContentLoaded', () => {
         }
     }
 
-    // 尝试刷新token
-    let isRefreshing = false; // 防止并发刷新
+    // 尝试刷新token(单飞模式,避免并发刷新导致状态错乱)
+    let isRefreshing = false;
+    let refreshPromise = null;
     async function tryRefreshToken () {
-        // 如果正在刷新,直接返回false
-        if (isRefreshing) {
-            return false;
+        // 如果已有刷新任务在进行,直接复用该任务结果
+        if (refreshPromise) {
+            return refreshPromise;
         }
 
-        try {
-            isRefreshing = true;
+        refreshPromise = (async () => {
+            if (isRefreshing) return false;
 
-            const token = localStorage.getItem('auth_token');
-            if (!token) {
-                return false;
-            }
+            try {
+                isRefreshing = true;
 
-            const response = await fetch('/api/auth/refresh', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                    'Authorization': `Bearer ${token}`
+                const token = localStorage.getItem('auth_token');
+                if (!token) {
+                    return false;
                 }
-            });
 
-            const data = await response.json();
+                // 使用原生fetch,避免被addAuthHeader包装后递归进入刷新逻辑
+                const nativeFetch = window.__nativeFetch || window.fetch;
+                const response = await nativeFetch('/api/auth/refresh', {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        'Authorization': `Bearer ${token}`
+                    }
+                });
+
+                const data = await response.json();
+
+                if (response.ok && data.success && data.token) {
+                    localStorage.setItem('auth_token', data.token);
+                    return true;
+                }
 
-            if (response.ok && data.success && data.token) {
-                localStorage.setItem('auth_token', data.token);
-                return true;
-            } else {
-                // 如果是401错误或token过期相关错误,直接返回false
                 if (response.status === 401 ||
                     data.code === 'TOKEN_EXPIRED_NO_SESSION' ||
                     data.code === 'MISSING_TOKEN') {
                     return false;
                 }
                 return false;
+            } catch (error) {
+                return false;
+            } finally {
+                isRefreshing = false;
+                refreshPromise = null;
             }
-        } catch (error) {
-            return false;
-        } finally {
-            isRefreshing = false;
-        }
+        })();
+
+        return refreshPromise;
     }
 
     function clearAuthAndRedirect () {
@@ -2044,14 +2053,26 @@ document.addEventListener('DOMContentLoaded', () => {
         if (userInfo) userInfo.style.display = 'flex';
     }
 
-    function addAuthHeader (token) {
-        // 为所有fetch请求添加认证头和错误处理
-        const originalFetch = window.fetch;
+    function addAuthHeader () {
+        // 避免重复包装fetch
+        if (!window.__nativeFetch) {
+            window.__nativeFetch = window.fetch;
+        }
+        if (window.__authFetchWrapped) {
+            return;
+        }
+        window.__authFetchWrapped = true;
+
+        const originalFetch = window.__nativeFetch;
         window.fetch = async function (url, options = {}) {
             if (options.headers === undefined) {
                 options.headers = {};
             }
-            options.headers['Authorization'] = `Bearer ${token}`;
+            // 每次请求都读取最新token,避免闭包持有旧token导致长期运行后401
+            const latestToken = localStorage.getItem('auth_token');
+            if (latestToken) {
+                options.headers['Authorization'] = `Bearer ${latestToken}`;
+            }
 
             try {
                 const response = await originalFetch(url, options);
@@ -2068,7 +2089,10 @@ document.addEventListener('DOMContentLoaded', () => {
                     const refreshed = await tryRefreshToken();
                     if (refreshed) {
                         // 更新token后重试请求
-                        options.headers['Authorization'] = `Bearer ${localStorage.getItem('auth_token')}`;
+                        const retryToken = localStorage.getItem('auth_token');
+                        if (retryToken) {
+                            options.headers['Authorization'] = `Bearer ${retryToken}`;
+                        }
                         return originalFetch(url, options);
                     } else {
                         // 刷新失败,清除认证信息并跳转登录

+ 55 - 6
templates/index.html

@@ -535,14 +535,63 @@
                 </div>
             </div>
         </div>
-    </div>
+    </div> 
+
+    <!-- 页面启动保护:避免脚本异常时出现纯白屏 -->
+    <script>
+        (function () {
+            function showFatalScreen(msg) {
+                if (document.getElementById('fatal-boot-error')) return;
+                const box = document.createElement('div');
+                box.id = 'fatal-boot-error';
+                box.style.cssText = 'position:fixed;inset:0;z-index:99999;background:#f8fafc;color:#1f2937;display:flex;align-items:center;justify-content:center;padding:24px;';
+                box.innerHTML = `
+                    <div style="max-width:680px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 10px 20px rgba(0,0,0,.08);padding:20px;">
+                        <h3 style="margin:0 0 10px;font-size:18px;">页面加载失败</h3>
+                        <p style="margin:0 0 12px;line-height:1.6;">${msg || '检测到关键脚本加载异常,已阻止白屏。请先刷新页面,若仍失败请联系管理员清理缓存并检查静态资源发布。'}</p>
+                        <div style="display:flex;gap:10px;">
+                            <button onclick="location.reload()" style="border:0;background:#4f46e5;color:#fff;border-radius:6px;padding:8px 14px;cursor:pointer;">刷新页面</button>
+                            <button onclick="location.href='/login'" style="border:1px solid #d1d5db;background:#fff;color:#111827;border-radius:6px;padding:8px 14px;cursor:pointer;">重新登录</button>
+                        </div>
+                    </div>
+                `;
+                document.body.appendChild(box);
+            }
+
+            window.__bootFailedScripts = [];
+            window.__onScriptLoadError = function (src) {
+                window.__bootFailedScripts.push(src || 'unknown');
+                showFatalScreen('关键静态资源加载失败:' + window.__bootFailedScripts.join(', '));
+            };
+
+            window.addEventListener('error', function (e) {
+                if (e && e.message) {
+                    showFatalScreen('页面初始化异常:' + e.message);
+                }
+            });
+
+            window.addEventListener('unhandledrejection', function (e) {
+                const reason = e && e.reason ? (e.reason.message || String(e.reason)) : '未知Promise异常';
+                showFatalScreen('页面初始化异常:' + reason);
+            });
+
+            window.addEventListener('load', function () {
+                setTimeout(function () {
+                    if (window.__bootFailedScripts.length > 0) return;
+                    if (typeof window.fetch !== 'function') {
+                        showFatalScreen('浏览器环境异常:fetch不可用。');
+                    }
+                }, 1200);
+            });
+        })();
+    </script>
 
     <!-- 核心JS库 -->
-    <script src="{{ url_for('static', filename='js/highlight.min.js') }}"></script>
-    <script src="{{ url_for('static', filename='js/marked.min.js') }}"></script>
-    <script src="{{ url_for('static', filename='js/showdown.min.js') }}"></script>
-    <script src="{{ url_for('static', filename='js/copy-buttons.js') }}"></script>
-    <script src="{{ url_for('static', filename='js/chat.js') }}"></script>
+    <script src="{{ url_for('static', filename='js/highlight.min.js') }}" onerror="window.__onScriptLoadError('highlight.min.js')"></script>
+    <script src="{{ url_for('static', filename='js/marked.min.js') }}" onerror="window.__onScriptLoadError('marked.min.js')"></script>
+    <script src="{{ url_for('static', filename='js/showdown.min.js') }}" onerror="window.__onScriptLoadError('showdown.min.js')"></script>
+    <script src="{{ url_for('static', filename='js/copy-buttons.js') }}" onerror="window.__onScriptLoadError('copy-buttons.js')"></script>
+    <script src="{{ url_for('static', filename='js/chat.js') }}" onerror="window.__onScriptLoadError('chat.js')"></script>
 
     <!-- 认证和用户信息管理 -->
     <!-- 自定义弹框 -->