From 3587ea4eaa7eb176143fa4d1898b49806decf2ed Mon Sep 17 00:00:00 2001 From: 2977094657 <2977094657@qq.com> Date: Wed, 11 Mar 2026 13:11:39 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(desktop-runtime):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=A1=8C=E9=9D=A2=E7=AB=AF=E9=87=8D=E8=A3=85=E5=90=8E?= =?UTF-8?q?=E4=BB=8D=E5=91=BD=E4=B8=AD=E6=97=A7=20UI=20=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启动打包版时按 UI buildId 检测并清理 renderer 缓存 - 记录 lastSeenUiBuildId,避免每次启动都重复清缓存 - 调整 SPA 静态资源缓存策略,index.html/200.html/_payload.json 禁止缓存 - 保留 _nuxt 哈希资源长期缓存,减少必须在卸载时删除数据后才生效的问题 --- desktop/src/main.cjs | 60 ++++++++++++++++++++++++++++++++++ src/wechat_decrypt_tool/api.py | 35 +++++++++++++++++--- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/desktop/src/main.cjs b/desktop/src/main.cjs index 1aeed76..3a0cb16 100644 --- a/desktop/src/main.cjs +++ b/desktop/src/main.cjs @@ -7,6 +7,7 @@ const { globalShortcut, dialog, shell, + session, } = require("electron"); let autoUpdater = null; let autoUpdaterLoadError = null; @@ -465,6 +466,34 @@ function getDesktopSettingsPath() { return path.join(dir, "desktop-settings.json"); } +function getPackagedUiDir() { + if (!app.isPackaged) return null; + try { + return path.join(process.resourcesPath, "ui"); + } catch { + return null; + } +} + +function readPackagedUiBuildId() { + const uiDir = getPackagedUiDir(); + if (!uiDir) return ""; + + try { + const indexPath = path.join(uiDir, "index.html"); + if (!fs.existsSync(indexPath)) return ""; + const html = fs.readFileSync(indexPath, { encoding: "utf8" }); + const match = + html.match(/buildId:"([^"]+)"/) || + html.match(/\/_payload\.json\?([^"'&<>\s]+)/) || + html.match(/data-src="\/_payload\.json\?([^"]+)"/); + return String(match?.[1] || "").trim(); + } catch (err) { + logMain(`[main] failed to read packaged UI build id: ${err?.message || err}`); + return ""; + } +} + function loadDesktopSettings() { if (desktopSettings) return desktopSettings; @@ -476,6 +505,9 @@ function loadDesktopSettings() { ignoredUpdateVersion: "", // Backend (FastAPI) listens on this port. Used in packaged builds. backendPort: DEFAULT_BACKEND_PORT, + // Tracks the packaged UI build so we can invalidate Chromium's HTTP cache + // after upgrades without wiping user data/localStorage. + lastSeenUiBuildId: "", }; const p = getDesktopSettingsPath(); @@ -539,6 +571,33 @@ function setIgnoredUpdateVersion(version) { return desktopSettings.ignoredUpdateVersion; } +async function refreshRendererCacheForPackagedUi() { + if (!app.isPackaged) return; + + const nextBuildId = readPackagedUiBuildId(); + if (!nextBuildId) return; + + const prevBuildId = String(loadDesktopSettings()?.lastSeenUiBuildId || "").trim(); + if (prevBuildId === nextBuildId) return; + + try { + const ses = session?.defaultSession; + if (ses) { + await ses.clearCache(); + try { + await ses.clearStorageData({ storages: ["serviceworkers"] }); + } catch {} + } + logMain(`[main] cleared renderer cache for UI build change: ${prevBuildId || "(none)"} -> ${nextBuildId}`); + } catch (err) { + logMain(`[main] failed to clear renderer cache for UI build change: ${err?.message || err}`); + } + + loadDesktopSettings(); + desktopSettings.lastSeenUiBuildId = nextBuildId; + persistDesktopSettings(); +} + function parseEnvBool(value) { if (value == null) return null; const v = String(value).trim().toLowerCase(); @@ -1614,6 +1673,7 @@ function registerWindowIpc() { async function main() { await app.whenReady(); + await refreshRendererCacheForPackagedUi(); Menu.setApplicationMenu(null); registerWindowIpc(); registerDebugShortcuts(); diff --git a/src/wechat_decrypt_tool/api.py b/src/wechat_decrypt_tool/api.py index 822ef1f..e342db9 100644 --- a/src/wechat_decrypt_tool/api.py +++ b/src/wechat_decrypt_tool/api.py @@ -99,9 +99,36 @@ class _SPAStaticFiles(StaticFiles): self._fallback_200 = Path(str(self.directory)) / "200.html" self._fallback_index = Path(str(self.directory)) / "index.html" - async def get_response(self, path: str, scope): # type: ignore[override] + @staticmethod + def _normalize_path(path: str) -> str: + return str(path or "").strip().lstrip("/") + + @classmethod + def _is_shell_path(cls, path: str) -> bool: + normalized = cls._normalize_path(path) + return normalized in {"", "index.html", "200.html", "_payload.json"} or normalized.startswith( + "_payload.json/" + ) + + @classmethod + def _apply_cache_headers(cls, path: str, response): + normalized = cls._normalize_path(path) try: - return await super().get_response(path, scope) + if cls._is_shell_path(normalized): + response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + elif normalized.startswith("_nuxt/"): + response.headers.setdefault("Cache-Control", "public, max-age=31536000, immutable") + except Exception: + pass + return response + + async def get_response(self, path: str, scope): # type: ignore[override] + normalized = self._normalize_path(path) + try: + response = await super().get_response(path, scope) + return self._apply_cache_headers(normalized, response) except StarletteHTTPException as exc: if exc.status_code != 404: raise @@ -112,8 +139,8 @@ class _SPAStaticFiles(StaticFiles): raise if self._fallback_200.exists(): - return FileResponse(str(self._fallback_200)) - return FileResponse(str(self._fallback_index)) + return self._apply_cache_headers("200.html", FileResponse(str(self._fallback_200))) + return self._apply_cache_headers("index.html", FileResponse(str(self._fallback_index))) def _maybe_mount_frontend() -> None: