mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 20:30:00 +08:00
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
210 lines
7.1 KiB
JavaScript
210 lines
7.1 KiB
JavaScript
/**
|
||
* 全局应用状态
|
||
* 管理 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) }
|
||
}
|