diff --git a/static/src/stores/resource.ts b/static/src/stores/resource.ts index d015976..94d3786 100644 --- a/static/src/stores/resource.ts +++ b/static/src/stores/resource.ts @@ -52,6 +52,9 @@ const buildDefaultQuota = (): UsageQuota => ({ role: 'user' }); +const PROJECT_STORAGE_POLL_INTERVAL_MS = 60_000; +const PROJECT_STORAGE_POLL_INTERVAL_MAX_MS = 300_000; + export const useResourceStore = defineStore('resource', { state: () => ({ tokenPanelCollapsed: true, @@ -75,6 +78,9 @@ export const useResourceStore = defineStore('resource', { lastContainerSample: null as ContainerSample | null, containerStatsTimer: null as ReturnType | null, projectStorageTimer: null as ReturnType | null, + projectStorageInFlight: false, + projectStorageFailCount: 0, + projectStorageIntervalMs: PROJECT_STORAGE_POLL_INTERVAL_MS, usageQuota: buildDefaultQuota(), usageQuotaTimer: null as ReturnType | null }), @@ -225,6 +231,10 @@ export const useResourceStore = defineStore('resource', { } }, async pollProjectStorage() { + if (this.projectStorageInFlight) { + return; + } + this.projectStorageInFlight = true; try { const resp = await fetch('/api/project-storage'); if (!resp.ok) { @@ -236,9 +246,26 @@ export const useResourceStore = defineStore('resource', { this.projectStorage.limit_bytes = data.data.limit_bytes ?? null; this.projectStorage.limit_label = data.data.limit_label || ''; this.projectStorage.usage_percent = data.data.usage_percent ?? null; + this.projectStorageFailCount = 0; + if (this.projectStorageIntervalMs > PROJECT_STORAGE_POLL_INTERVAL_MS) { + this._restartProjectStorageTimer(PROJECT_STORAGE_POLL_INTERVAL_MS); + } } } catch (err) { + this.projectStorageFailCount += 1; console.warn('获取存储信息失败:', err); + if (this.projectStorageFailCount >= 3) { + const slower = Math.min( + this.projectStorageIntervalMs * 2, + PROJECT_STORAGE_POLL_INTERVAL_MAX_MS + ); + if (slower !== this.projectStorageIntervalMs) { + console.warn(`连续获取失败,存储轮询间隔调整为 ${Math.round(slower / 1000)} 秒`); + this._restartProjectStorageTimer(slower); + } + } + } finally { + this.projectStorageInFlight = false; } }, startProjectStoragePolling() { @@ -248,13 +275,25 @@ export const useResourceStore = defineStore('resource', { this.pollProjectStorage(); this.projectStorageTimer = setInterval(() => { this.pollProjectStorage(); - }, 5000); + }, this.projectStorageIntervalMs); }, stopProjectStoragePolling() { if (this.projectStorageTimer) { clearInterval(this.projectStorageTimer); this.projectStorageTimer = null; } + this.projectStorageInFlight = false; + this.projectStorageFailCount = 0; + this.projectStorageIntervalMs = PROJECT_STORAGE_POLL_INTERVAL_MS; + }, + _restartProjectStorageTimer(intervalMs: number) { + if (this.projectStorageTimer) { + clearInterval(this.projectStorageTimer); + } + this.projectStorageIntervalMs = Math.max(intervalMs, 10_000); + this.projectStorageTimer = setInterval(() => { + this.pollProjectStorage(); + }, this.projectStorageIntervalMs); }, normalizeUsageQuota(raw: any) { const base = buildDefaultQuota(); diff --git a/web_server.py b/web_server.py index eed9756..c0f5ff6 100644 --- a/web_server.py +++ b/web_server.py @@ -172,6 +172,8 @@ CSRF_EXEMPT_PATHS = {"/api/csrf-token"} FAILED_LOGIN_LIMIT = 5 FAILED_LOGIN_LOCK_SECONDS = 300 SOCKET_TOKEN_TTL_SECONDS = 45 +PROJECT_STORAGE_CACHE: Dict[str, Dict[str, Any]] = {} +PROJECT_STORAGE_CACHE_TTL_SECONDS = float(os.environ.get("PROJECT_STORAGE_CACHE_TTL", "30")) def sanitize_filename_preserve_unicode(filename: str) -> str: @@ -1147,6 +1149,10 @@ def get_container_status_api(terminal: WebTerminal, workspace: UserWorkspace, us @with_terminal def get_project_storage(terminal: WebTerminal, workspace: UserWorkspace, username: str): """获取项目目录占用情况,供前端轮询。""" + now = time.time() + cache_entry = PROJECT_STORAGE_CACHE.get(username) + if cache_entry and (now - cache_entry.get("ts", 0)) < PROJECT_STORAGE_CACHE_TTL_SECONDS: + return jsonify({"success": True, "data": cache_entry["data"]}) try: file_manager = getattr(terminal, 'file_manager', None) if not file_manager: @@ -1160,8 +1166,12 @@ def get_project_storage(terminal: WebTerminal, workspace: UserWorkspace, usernam "limit_label": f"{PROJECT_MAX_STORAGE_MB}MB" if PROJECT_MAX_STORAGE_MB else "未限制", "usage_percent": usage_percent } + PROJECT_STORAGE_CACHE[username] = {"ts": now, "data": data} return jsonify({"success": True, "data": data}) except Exception as exc: + stale = PROJECT_STORAGE_CACHE.get(username) + if stale: + return jsonify({"success": True, "data": stale.get("data"), "stale": True}), 200 return jsonify({"success": False, "error": str(exc)}), 500