fix: #145 仪表盘版本缓存 + #144 macOS手动安装检测 + #146 更新提示持久化 + #148 AI助手Web模式CORS

- dashboard.js: 版本/状态信息持久化缓存,实例切换时自动清空 (#145)
- service.rs: macOS is_cli_installed 改为真实路径探测,无plist时返回默认Gateway条目 (#144)
- utils.rs: 新增 common_non_windows_cli_candidates,resolve_openclaw_cli_path 优先检测 standalone/手动安装路径 (#144)
- config.rs: macOS get_local_version 增加 resolve_cli + canonicalize,detect_installed_source 增加 CLI 分类 + standalone 检测 (#144)
- dev-api.js: macOS CLI/版本/来源检测补齐 Intel Homebrew + standalone + findOpenclawBin (#144)
- main.js: 更新 banner dismiss 从 sessionStorage 改 localStorage (#146)
- assistant.js: Web 模式 AI 测试走后端代理 api.testModel 绕过 CORS (#148)
This commit is contained in:
晴天
2026-03-26 02:02:19 +08:00
parent 038e9c01bc
commit 7de40624f7
7 changed files with 159 additions and 16 deletions

View File

@@ -639,7 +639,7 @@ async function checkGlobalUpdate() {
if (!ver) return
// 用户已忽略过该版本,不再打扰
const dismissed = sessionStorage.getItem('clawpanel_update_dismissed')
const dismissed = localStorage.getItem('clawpanel_update_dismissed')
if (dismissed === ver) return
const changelog = info.manifest?.changelog || ''
@@ -665,7 +665,7 @@ async function checkGlobalUpdate() {
// 关闭按钮:记住忽略的版本
banner.querySelector('#btn-update-dismiss')?.addEventListener('click', () => {
sessionStorage.setItem('clawpanel_update_dismissed', ver)
localStorage.setItem('clawpanel_update_dismissed', ver)
banner.classList.add('update-banner-hidden')
})

View File

@@ -3168,6 +3168,22 @@ function showSettings() {
btn.disabled = true
btn.textContent = t('assistant.testing')
resultEl.innerHTML = '<span style="color:var(--text-tertiary)">' + t('assistant.testSending') + '</span>'
// Web 模式下浏览器 fetch 受 CORS 限制,优先走后端代理
if (!window.__TAURI_INTERNALS__) {
const t0 = Date.now()
try {
const reply = await api.testModel(baseUrl, apiKey, model, selApiType)
const elapsed = Date.now() - t0
resultEl.innerHTML = buildTestResult({ success: true, elapsed, usedApi: selApiType, reqUrl: baseUrl, reqBody: {}, respStatus: 200, respBody: '', reply: reply || '(ok)' })
} catch (err) {
const elapsed = Date.now() - t0
resultEl.innerHTML = buildTestResult({ success: false, elapsed, usedApi: selApiType, reqUrl: baseUrl, reqBody: {}, respStatus: 0, respBody: '', error: err.message || String(err) })
}
btn.disabled = false; btn.textContent = t('assistant.testBtn')
return
}
const base = cleanBaseUrl(baseUrl, selApiType)
const hdrs = authHeaders(selApiType, apiKey)
const t0 = Date.now()

View File

@@ -3,7 +3,7 @@
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { onGatewayChange } from '../lib/app-state.js'
import { getActiveInstance, onGatewayChange } from '../lib/app-state.js'
import { navigate } from '../router.js'
import { t } from '../lib/i18n.js'
@@ -65,8 +65,22 @@ export function cleanup() {
}
let _dashboardInitialized = false
let _dashboardVersionCache = null
let _dashboardStatusSummaryCache = null
let _dashboardInstanceId = ''
function syncDashboardInstanceScope() {
const instanceId = getActiveInstance()?.id || 'local'
if (_dashboardInstanceId && _dashboardInstanceId !== instanceId) {
_dashboardInitialized = false
_dashboardVersionCache = null
_dashboardStatusSummaryCache = null
}
_dashboardInstanceId = instanceId
}
async function loadDashboardData(page, fullRefresh = false) {
syncDashboardInstanceScope()
// 分波加载:关键数据先渲染,次要数据后填充,减少白屏等待
// 轻量调用读文件每次都做重量调用spawn CLI/网络请求)只在首次或手动刷新时做
const withTimeout = (promise, ms) => Promise.race([
@@ -77,21 +91,23 @@ async function loadDashboardData(page, fullRefresh = false) {
api.getServicesStatus(),
api.readOpenclawConfig(),
// 版本信息:首次加载或手动刷新时才查询(避免 ARM 设备上频繁查 npm registry
(!_dashboardInitialized || fullRefresh) ? api.getVersionInfo() : Promise.resolve(null),
(!_dashboardInitialized || fullRefresh || !_dashboardVersionCache) ? api.getVersionInfo() : Promise.resolve(_dashboardVersionCache),
]), 15000)
const secondaryP = withTimeout(Promise.allSettled([
api.listAgents(),
api.readMcpConfig(),
api.listBackups(),
// getStatusSummary 是最重的调用spawn openclaw status --json只在首次加载时调用
(!_dashboardInitialized || fullRefresh) ? api.getStatusSummary() : Promise.resolve(null),
(!_dashboardInitialized || fullRefresh || !_dashboardStatusSummaryCache) ? api.getStatusSummary() : Promise.resolve(_dashboardStatusSummaryCache),
]), 15000).catch(() => [{ status: 'rejected' }, { status: 'rejected' }, { status: 'rejected' }, { status: 'rejected' }])
const logsP = api.readLogTail('gateway', 20).catch(() => '')
// 第一波:服务状态 + 配置 + 版本 → 立即渲染统计卡片
const [servicesRes, configRes, versionRes] = await coreP
const services = servicesRes.status === 'fulfilled' ? servicesRes.value : []
const version = (versionRes.status === 'fulfilled' && versionRes.value) ? versionRes.value : {}
const version = (versionRes.status === 'fulfilled' && versionRes.value)
? (_dashboardVersionCache = versionRes.value)
: (_dashboardVersionCache || {})
const config = configRes.status === 'fulfilled' ? configRes.value : null
if (servicesRes.status === 'rejected') toast(t('dashboard.servicesLoadFail'), 'error')
if (versionRes.status === 'rejected') toast(t('dashboard.versionLoadFail'), 'error')
@@ -128,7 +144,9 @@ async function loadDashboardData(page, fullRefresh = false) {
const agents = agentsRes.status === 'fulfilled' ? agentsRes.value : []
const mcpConfig = mcpRes.status === 'fulfilled' ? mcpRes.value : null
const backups = backupsRes.status === 'fulfilled' ? backupsRes.value : []
const statusSummary = statusRes.status === 'fulfilled' ? statusRes.value : null
const statusSummary = (statusRes.status === 'fulfilled' && statusRes.value)
? (_dashboardStatusSummaryCache = statusRes.value)
: _dashboardStatusSummaryCache
renderStatCards(page, services, version, agents, config)
renderOverview(page, services, mcpConfig, backups, config, agents, statusSummary)
@@ -475,6 +493,7 @@ function bindActions(page) {
btnUpdate.textContent = t('dashboard.checking')
try {
const info = await api.getVersionInfo()
_dashboardVersionCache = info
if (info.ahead_of_recommended && info.recommended) {
toast(t('dashboard.versionAheadWarn', { current: info.current || '', recommended: info.recommended }), 'warning')
} else if (info.update_available && info.recommended) {