Compare commits

...

1 Commits

  • fix(desktop-runtime): 修复桌面端重装后仍命中旧 UI 缓存
    - 启动打包版时按 UI buildId 检测并清理 renderer 缓存
    - 记录 lastSeenUiBuildId,避免每次启动都重复清缓存
    - 调整 SPA 静态资源缓存策略,index.html/200.html/_payload.json 禁止缓存
    - 保留 _nuxt 哈希资源长期缓存,减少必须在卸载时删除数据后才生效的问题
2 changed files with 91 additions and 4 deletions
+60
View File
@@ -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();
+31 -4
View File
@@ -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: