Files
clawpanel/src/lib/app-state.js
晴天 70d768be17 feat: new pages + dashboard enhancements + backend improvements
New pages:
- Plugin Hub: grid cards, search, install/toggle/enable plugins
- Route Map: SVG visualization of channels→agents bindings with legends
- Diagnose: gateway connectivity diagnosis with step-by-step checks

Dashboard enhancements:
- WebSocket status indicator (connected/handshaking/reconnecting/disconnected)
- Connected channels overview with platform icons
- Colored log level badges (ERROR/WARN/INFO/DEBUG) with timestamps
- Channels data loading in dashboard secondary fetch

Splash screen:
- Multi-stage boot detection (JS not loaded vs boot slow vs timeout)
- 15s: WebView2/resource load failure
- 20s: "initializing..." hint with elapsed counter
- 90s: true timeout error

Backend (Rust):
- diagnose.rs: gateway connectivity diagnosis command
- messaging.rs: plugin management commands
- service.rs: improvements
- lib.rs: register new commands

Frontend libs:
- feature-gates.js: feature flag system
- ws-client.js: reconnect state tracking
- tauri-api.js: new API bindings
- model-presets.js: provider fixes
- Remove gateway-guardian-policy.js (unused)

Dev API (scripts/dev-api.js):
- list_all_plugins, toggle_plugin, install_plugin handlers
- probe_gateway_port, diagnose_gateway_connection handlers

i18n: dashboard, sidebar, diagnose, extensions, routeMap locale modules
CSS: plugin-hub cards, route-map SVG styles
2026-04-11 00:44:06 +08:00

210 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 全局应用状态
* 管理 openclaw 安装状态,供各组件查询
*/
import { api } from './tauri-api.js'
const isTauri = !!window.__TAURI_INTERNALS__
let _openclawReady = false
let _gatewayRunning = false
let _gatewayForeign = false
let _platform = '' // 'macos' | 'win32' | ...
let _deployMode = 'local' // 'local' | 'docker'
let _inDocker = false
let _dockerAvailable = false
let _listeners = []
let _gwListeners = []
let _gwStopCount = 0 // 连续检测到"停止"的次数,防抖用
let _isUpgrading = false // 升级/切换版本期间,阻止 setup 跳转
let _userStopped = false // 用户主动停止,不自动拉起
let _gatewayRunningSince = 0 // Gateway 最近一次进入稳定运行状态的时间
let _guardianListeners = [] // 守护放弃时的回调(后端 guardian-event 触发)
/** openclaw 是否就绪CLI 已安装 + 配置文件存在) */
export function isOpenclawReady() {
// 升级期间视为就绪,避免跳转到 setup
if (_isUpgrading) return true
return _openclawReady
}
/** 标记升级中(阻止 setup 跳转) */
export function setUpgrading(v) { _isUpgrading = !!v }
export function isUpgrading() { return _isUpgrading }
/** 标记用户主动停止 Gateway不触发自动重启 */
export function setUserStopped(v) { _userStopped = !!v }
/** 重置守护状态(用户手动启动后重置) */
export function resetAutoRestart() {
_gatewayRunningSince = 0
_userStopped = false
}
/** 监听守护放弃事件连续重启失败后触发UI 可弹出恢复选项) */
export function onGuardianGiveUp(fn) {
_guardianListeners.push(fn)
return () => { _guardianListeners = _guardianListeners.filter(cb => cb !== fn) }
}
/** Gateway 是否正在运行(仅 owned */
export function isGatewayRunning() {
return _gatewayRunning
}
/** Gateway 是否在运行但属于外部实例 */
export function isGatewayForeign() {
return _gatewayForeign
}
/** 获取后端平台 ('macos' | 'win32') */
export function getPlatform() {
return _platform
}
export function isMacPlatform() {
return _platform === 'macos'
}
/** 部署模式 */
export function getDeployMode() { return _deployMode }
export function isInDocker() { return _inDocker }
export function isDockerAvailable() { return _dockerAvailable }
/** 实例管理 */
let _activeInstance = { id: 'local', name: '本机', type: 'local' }
let _instanceListeners = []
export function getActiveInstance() { return _activeInstance }
export function isLocalInstance() { return _activeInstance.type === 'local' }
export function onInstanceChange(fn) {
_instanceListeners.push(fn)
return () => { _instanceListeners = _instanceListeners.filter(cb => cb !== fn) }
}
export async function switchInstance(id) {
// instanceSetActive 内部已调用 _cache.clear(),切换后所有缓存自动失效
await api.instanceSetActive(id)
const data = await api.instanceList()
_activeInstance = data.instances.find(i => i.id === id) || data.instances[0]
_instanceListeners.forEach(fn => { try { fn(_activeInstance) } catch {} })
}
export async function loadActiveInstance() {
try {
const data = await api.instanceList()
_activeInstance = data.instances.find(i => i.id === data.activeId) || data.instances[0]
} catch {
_activeInstance = { id: 'local', name: '本机', type: 'local' }
}
}
/** 监听 Gateway 状态变化 */
export function onGatewayChange(fn) {
_gwListeners.push(fn)
return () => { _gwListeners = _gwListeners.filter(cb => cb !== fn) }
}
/** 检测 openclaw 安装状态 */
export async function detectOpenclawStatus() {
try {
const [installation, services] = await Promise.allSettled([
api.checkInstallation(),
api.getServicesStatus(),
])
const configExists = installation.status === 'fulfilled' && installation.value?.installed
if (installation.status === 'fulfilled' && installation.value?.platform) {
_platform = installation.value.platform
}
if (installation.status === 'fulfilled' && installation.value?.inDocker) {
_inDocker = true
_deployMode = 'docker'
}
const cliInstalled = services.status === 'fulfilled'
&& services.value?.length > 0
&& services.value[0]?.cli_installed !== false
_openclawReady = configExists && cliInstalled
// 顺便检测 Gateway 运行状态
if (services.status === 'fulfilled' && services.value?.length > 0) {
const gw = services.value.find?.(s => s.label === 'ai.openclaw.gateway') || services.value[0]
const foreign = gw?.running === true && gw?.owned_by_current_instance === false
_setGatewayRunning(gw?.running === true && !foreign, foreign)
}
} catch {
_openclawReady = false
}
_listeners.forEach(fn => { try { fn(_openclawReady) } catch {} })
return _openclawReady
}
function _setGatewayRunning(val, foreign = false) {
const wasRunning = _gatewayRunning
const wasForeign = _gatewayForeign
const changed = wasRunning !== val || wasForeign !== foreign
_gatewayRunning = val
_gatewayForeign = foreign
if (changed) {
if (val) {
// 仅记录恢复运行时间,避免短暂存活就把重启计数清零
_gatewayRunningSince = Date.now()
} else if (wasRunning && !_userStopped && !_isUpgrading && _openclawReady && !foreign) {
_gatewayRunningSince = 0
// Gateway 意外停止 → 后端 Rust guardian 负责自动重启,前端仅更新 UI 状态
console.log('[app-state] Gateway 意外停止,等待后端 guardian 重启...')
} else if (!val) {
_gatewayRunningSince = 0
}
_gwListeners.forEach(fn => { try { fn(val, foreign) } catch {} })
}
}
/** 刷新 Gateway 运行状态(轻量,仅查服务状态)
* 防抖running→stopped 需要连续 3 次检测才切换,避免瞬态误判 */
export async function refreshGatewayStatus() {
try {
const services = await api.getServicesStatus()
if (services?.length > 0) {
const gw = services.find?.(s => s.label === 'ai.openclaw.gateway') || services[0]
const ownedRunning = gw?.running === true && gw?.owned_by_current_instance !== false
const foreignRunning = gw?.running === true && gw?.owned_by_current_instance === false
const nowRunning = ownedRunning
if (nowRunning) {
_gwStopCount = 0
if (!_gatewayRunning) {
_setGatewayRunning(true, false)
}
} else {
if (foreignRunning) {
_gwStopCount = 0
} else {
_gwStopCount++
}
if (foreignRunning || _gwStopCount >= 3 || !_gatewayRunning) {
_setGatewayRunning(false, foreignRunning)
}
}
}
} catch {
_gwStopCount++
if (_gwStopCount >= 3) _setGatewayRunning(false)
}
return _gatewayRunning
}
let _pollTimer = null
/** 启动 Gateway 状态轮询(每 15 秒检测一次) */
export function startGatewayPoll() {
if (_pollTimer) return
_pollTimer = setInterval(() => refreshGatewayStatus(), 15000)
}
export function stopGatewayPoll() {
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null }
}
/** 监听状态变化 */
export function onReadyChange(fn) {
_listeners.push(fn)
return () => { _listeners = _listeners.filter(cb => cb !== fn) }
}