From c1f8c130d29e6df4378583efb7111aefe7ce17f7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 11:06:50 +0000 Subject: [PATCH] fix(dashboard): serialize loadDashboardData to prevent config write races MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fullRefresh bypassed the in-flight guard, so a manual refresh could overlap the initial dashboard load. Two concurrent _loadDashboardDataInner runs could both read/patch/write openclaw.json and clobber each other. Queue loads on a shared promise chain so only one inner load runs at a time. Co-authored-by: 晴天 <1186258278@users.noreply.github.com> --- src/pages/dashboard.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pages/dashboard.js b/src/pages/dashboard.js index b480bc8..28c95a6 100644 --- a/src/pages/dashboard.js +++ b/src/pages/dashboard.js @@ -13,7 +13,8 @@ import { attachCliConflictBanner } from '../components/cli-conflict-banner.js' import { icon } from '../lib/icons.js' let _unsubGw = null -let _loadInFlight = false +/** 串行化仪表盘加载:fullRefresh 曾绕过 in-flight 检查,可与首屏加载并发,双写配置竞态。 */ +let _dashboardLoadChain = Promise.resolve() let _lastGwChangeLoad = 0 let _detachCliConflict = null @@ -197,12 +198,12 @@ function normalizeDefaultModelConfig(config) { return modelConfig.primary } -async function loadDashboardData(page, fullRefresh = false) { - // 并发保护:如果上一次加载仍在进行,跳过本次(fullRefresh 除外) - if (_loadInFlight && !fullRefresh) return - const loadSeq = ++_dashboardLoadSeq - _loadInFlight = true - try { await _loadDashboardDataInner(page, fullRefresh, loadSeq) } finally { if (loadSeq === _dashboardLoadSeq) _loadInFlight = false } +function loadDashboardData(page, fullRefresh = false) { + _dashboardLoadChain = _dashboardLoadChain.catch(() => {}).then(async () => { + const loadSeq = ++_dashboardLoadSeq + await _loadDashboardDataInner(page, fullRefresh, loadSeq) + }) + return _dashboardLoadChain } async function _loadDashboardDataInner(page, fullRefresh, loadSeq) {