From 8a247af03e13f0d4700ee205f87b4704123e90dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Sun, 5 Jul 2026 12:14:16 -0700 Subject: [PATCH] =?UTF-8?q?fix(setup):=20=E6=9C=AA=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=B8=8D=E5=86=8D=E5=8F=8D=E5=A4=8D=E8=B7=B3?= =?UTF-8?q?=E5=9B=9E=E6=A3=80=E6=B5=8B=E9=A1=B5=E3=80=81=E8=81=9A=E7=84=A6?= =?UTF-8?q?=E9=87=8D=E6=A3=80=E4=B8=8D=E5=86=8D=E6=B8=85=E7=A9=BA=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈"未安装时系统一直在检测/跳回检测页",三个成因逐一修复: - 安装/升级任务事件(upgrade-done/error)回调在 OpenClaw 未就绪时 无条件 navigate('/setup'):安装失败会把用户从诊断/设置页拽回安装页, 已在 setup 页时重复 navigate 还会整页重建重新检测。 现只把默认路由指向 setup,仅当停留在依赖 CLI 的仪表盘时才主动跳转 - 窗口聚焦触发的重检(#Compat-4)每次都把步骤区清成骨架屏, 切窗口回来就像"又从头检测"。聚焦重检改为 silent 模式: 保留已渲染内容,检测完成后原地更新 - 检测流程加异常兜底:任何错误渲染错误卡片并提示重新检测, 骨架屏不再有机会永久残留 Co-Authored-By: Claude Fable 5 --- src/main.js | 10 ++++- src/pages/setup.js | 108 ++++++++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 47 deletions(-) diff --git a/src/main.js b/src/main.js index d8536ad..d654bff 100644 --- a/src/main.js +++ b/src/main.js @@ -874,10 +874,16 @@ async function boot() { if (isOpenclawReady() && window.location.hash === '#/setup') { navigate('/dashboard') } - // 如果卸载后变为未就绪,跳转到 setup + // 如果卸载后变为未就绪,把默认路由指向 setup; + // 但只有停留在依赖 CLI 的页面(仪表盘)才主动跳转, + // 避免安装失败事件把用户从诊断/设置等页面反复拽回安装页、 + // 或在 setup 页上重复 navigate 导致页面反复重建重新检测 if (!isOpenclawReady() && !isUpgrading()) { setDefaultRoute('/setup') - navigate('/setup') + const current = window.location.hash.replace(/^#/, '') + if (current === '/dashboard' || current === '' || current === '/') { + navigate('/setup') + } } window.dispatchEvent(new CustomEvent('openclaw:runtime-changed')) } diff --git a/src/pages/setup.js b/src/pages/setup.js index 5c5c172..8701a57 100644 --- a/src/pages/setup.js +++ b/src/pages/setup.js @@ -184,7 +184,8 @@ export async function render() { const now = Date.now() if (now - _lastRedetectAt < 3000) return _lastRedetectAt = now - runDetect(page) + // 聚焦重检走 silent:需要刷新缓存(可能刚装完 Node/Git),但不清空已渲染内容 + runDetect(page, { silent: true }) } document.addEventListener('visibilitychange', onVisibilityChange) window.addEventListener('focus', onVisibilityChange) @@ -242,15 +243,20 @@ async function promptRestart(msg) { // 检测序号:并发触发(首次加载 / 重新检测 / 窗口聚焦)时,只允许最新一次的结果上屏 let _detectSeq = 0 -async function runDetect(page, { fresh = true } = {}) { +async function runDetect(page, { fresh = true, silent = false } = {}) { const seq = ++_detectSeq const stepsEl = page.querySelector('#setup-steps') - stepsEl.innerHTML = ` -
-
-
-
- ` + // silent(窗口聚焦触发的后台重检):保留已渲染的步骤卡片,检测完成后原地更新, + // 不再每次聚焦都清成骨架屏,避免"一直在检测"的观感 + const hasRenderedContent = stepsEl.children.length > 0 && !stepsEl.querySelector('.loading-placeholder') + if (!silent || !hasRenderedContent) { + stepsEl.innerHTML = ` +
+
+
+
+ ` + } // 首次进入页面(fresh=false)跳过缓存清理:进程刚启动时 PATH 缓存是新鲜的, // 还能复用启动流程已缓存的检测结果,让首屏尽快出现; // 手动「重新检测」、窗口聚焦重检和安装后的重试仍然完整清理(fresh=true)。 @@ -262,46 +268,58 @@ async function runDetect(page, { fresh = true } = {}) { // 必须先调此命令扫描文件系统新装路径,才能让 where/which 找到新二进制。 try { await api.invalidatePathCache() } catch {} } - // 版本信息包含 npm registry 网络查询与全盘安装扫描,是最慢的一项; - // 步骤卡片的通过/失败判定不依赖它,先用本地检测结果渲染,版本详情返回后再补充 - const versionPromise = api.getVersionInfo().catch(() => null) - // 并行检测 Node.js、Git、OpenClaw CLI、配置文件 - const [nodeRes, gitRes, clawRes, configRes] = await Promise.allSettled([ - api.checkNode(), - api.checkGit(), - api.getServicesStatus(), - api.checkInstallation(), - ]) - if (seq !== _detectSeq || !page.isConnected) return + try { + // 版本信息包含 npm registry 网络查询与全盘安装扫描,是最慢的一项; + // 步骤卡片的通过/失败判定不依赖它,先用本地检测结果渲染,版本详情返回后再补充 + const versionPromise = api.getVersionInfo().catch(() => null) + // 并行检测 Node.js、Git、OpenClaw CLI、配置文件 + const [nodeRes, gitRes, clawRes, configRes] = await Promise.allSettled([ + api.checkNode(), + api.checkGit(), + api.getServicesStatus(), + api.checkInstallation(), + ]) + if (seq !== _detectSeq || !page.isConnected) return - const node = nodeRes.status === 'fulfilled' ? nodeRes.value : { installed: false } - const git = gitRes.status === 'fulfilled' ? gitRes.value : { installed: false } - const cliOk = clawRes.status === 'fulfilled' - && clawRes.value?.length > 0 - && clawRes.value[0]?.cli_installed !== false - let config = configRes.status === 'fulfilled' ? configRes.value : { installed: false } + const node = nodeRes.status === 'fulfilled' ? nodeRes.value : { installed: false } + const git = gitRes.status === 'fulfilled' ? gitRes.value : { installed: false } + const cliOk = clawRes.status === 'fulfilled' + && clawRes.value?.length > 0 + && clawRes.value[0]?.cli_installed !== false + let config = configRes.status === 'fulfilled' ? configRes.value : { installed: false } - // Git 已安装时,自动配置 HTTPS 替代 SSH(静默执行) - if (git.installed) { - api.configureGitHttps().catch(() => {}) + // Git 已安装时,自动配置 HTTPS 替代 SSH(静默执行) + if (git.installed) { + api.configureGitHttps().catch(() => {}) + } + + const nodeOk = node.installed && node.compatible !== false + const allOk = nodeOk && cliOk && config.installed + + // 全部通过 → 自动跳转到仪表盘 + if (allOk) { + const engine = getActiveEngine() + if (engine?.detect) await engine.detect() + window.location.hash = '/dashboard' + return + } + + renderSteps(page, { node, git, cliOk, config, version: null }) + + const version = await versionPromise + if (seq !== _detectSeq || !page.isConnected || !version) return + renderSteps(page, { node, git, cliOk, config, version }) + } catch (error) { + // 兜底:任何异常都不能让骨架屏永久残留 + console.error('[setup] 环境检测失败', error) + if (seq !== _detectSeq || !page.isConnected) return + stepsEl.innerHTML = ` +
+ ${escapeHtml(error?.message || String(error))} +
${escapeHtml(t('setup.recheck'))}
+
+ ` } - - const nodeOk = node.installed && node.compatible !== false - const allOk = nodeOk && cliOk && config.installed - - // 全部通过 → 自动跳转到仪表盘 - if (allOk) { - const engine = getActiveEngine() - if (engine?.detect) await engine.detect() - window.location.hash = '/dashboard' - return - } - - renderSteps(page, { node, git, cliOk, config, version: null }) - - const version = await versionPromise - if (seq !== _detectSeq || !page.isConnected || !version) return - renderSteps(page, { node, git, cliOk, config, version }) } function stepIcon(ok) {