v0.4.0: Gateway 进程守护、配置自愈、双配置同步、流式超时、模型删除安全切换

This commit is contained in:
晴天
2026-03-05 20:44:47 +08:00
parent d27d5cc8af
commit 79cd15e1c4
30 changed files with 2257 additions and 295 deletions

View File

@@ -6,19 +6,54 @@ import { api } from './tauri-api.js'
let _openclawReady = false
let _gatewayRunning = false
let _platform = '' // 'macos' | 'win32' | ...
let _listeners = []
let _gwListeners = []
let _gwStopCount = 0 // 连续检测到"停止"的次数,防抖用
let _isUpgrading = false // 升级/切换版本期间,阻止 setup 跳转
let _userStopped = false // 用户主动停止,不自动拉起
let _autoRestartCount = 0 // 自动重启次数
let _lastRestartTime = 0 // 上次重启时间
const MAX_AUTO_RESTART = 3 // 最大连续自动重启次数
const RESTART_COOLDOWN = 60000 // 重启冷却期 60s
let _guardianListeners = [] // 守护放弃时的回调
/** 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() { _autoRestartCount = 0; _userStopped = false }
/** 监听守护放弃事件连续重启失败后触发UI 可弹出恢复选项) */
export function onGuardianGiveUp(fn) {
_guardianListeners.push(fn)
return () => { _guardianListeners = _guardianListeners.filter(cb => cb !== fn) }
}
/** Gateway 是否正在运行 */
export function isGatewayRunning() {
return _gatewayRunning
}
/** 获取后端平台 ('macos' | 'win32') */
export function getPlatform() {
return _platform
}
export function isMacPlatform() {
return _platform === 'macos'
}
/** 监听 Gateway 状态变化 */
export function onGatewayChange(fn) {
_gwListeners.push(fn)
@@ -33,6 +68,9 @@ export async function detectOpenclawStatus() {
api.getServicesStatus(),
])
const configExists = installation.status === 'fulfilled' && installation.value?.installed
if (installation.status === 'fulfilled' && installation.value?.platform) {
_platform = installation.value.platform
}
const cliInstalled = services.status === 'fulfilled'
&& services.value?.length > 0
&& services.value[0]?.cli_installed !== false
@@ -50,17 +88,62 @@ export async function detectOpenclawStatus() {
}
function _setGatewayRunning(val) {
const changed = _gatewayRunning !== val
const wasRunning = _gatewayRunning
const changed = wasRunning !== val
_gatewayRunning = val
if (changed) _gwListeners.forEach(fn => { try { fn(val) } catch {} })
if (changed) {
if (val) {
// Gateway 恢复运行,重置计数
_autoRestartCount = 0
} else if (wasRunning && !_userStopped && !_isUpgrading && _openclawReady) {
// Gateway 意外停止,尝试自动重启
_tryAutoRestart()
}
_gwListeners.forEach(fn => { try { fn(val) } catch {} })
}
}
/** 刷新 Gateway 运行状态(轻量,仅查服务状态) */
async function _tryAutoRestart() {
const now = Date.now()
// 冷却期内不重复重启
if (now - _lastRestartTime < RESTART_COOLDOWN) return
if (_autoRestartCount >= MAX_AUTO_RESTART) {
console.warn(`[guardian] Gateway 已连续自动重启 ${MAX_AUTO_RESTART} 次,停止守护,请手动检查`)
_guardianListeners.forEach(fn => { try { fn() } catch {} })
return
}
_autoRestartCount++
_lastRestartTime = now
console.log(`[guardian] Gateway 意外停止,自动重启 (${_autoRestartCount}/${MAX_AUTO_RESTART})...`)
try {
await api.startService('ai.openclaw.gateway')
console.log('[guardian] Gateway 自动重启成功')
} catch (e) {
console.error('[guardian] Gateway 自动重启失败:', e)
}
}
/** 刷新 Gateway 运行状态(轻量,仅查服务状态)
* 防抖running→stopped 需要连续 2 次检测才切换,避免瞬态误判 */
export async function refreshGatewayStatus() {
try {
const services = await api.getServicesStatus()
if (services?.length > 0) _setGatewayRunning(services[0]?.running === true)
} catch {}
if (services?.length > 0) {
const nowRunning = services[0]?.running === true
if (nowRunning) {
_gwStopCount = 0
_setGatewayRunning(true)
} else {
_gwStopCount++
if (_gwStopCount >= 2 || !_gatewayRunning) {
_setGatewayRunning(false)
}
}
}
} catch {
_gwStopCount++
if (_gwStopCount >= 2) _setGatewayRunning(false)
}
return _gatewayRunning
}

View File

@@ -5,6 +5,17 @@
const isTauri = !!window.__TAURI_INTERNALS__
// 写操作不应静默回退 mock否则会“假成功”
const NO_MOCK_CMDS = new Set([
'start_service', 'stop_service', 'restart_service',
'upgrade_openclaw', 'install_gateway', 'uninstall_gateway',
'write_openclaw_config', 'write_mcp_config',
'create_backup', 'restore_backup', 'delete_backup',
'write_memory_file', 'delete_memory_file',
'set_npm_registry', 'reload_gateway', 'restart_gateway',
'auto_pair_device',
])
// 预加载 Tauri invoke避免每次 API 调用都做动态 import
const _invokeReady = isTauri
? import('@tauri-apps/api/core').then(m => m.invoke)
@@ -74,10 +85,38 @@ async function invoke(cmd, args = {}) {
logRequest(cmd, args, duration, false)
return result
}
const result = mockInvoke(cmd, args)
const duration = Date.now() - start
logRequest(cmd, args, duration, false)
return result
// Web 模式:优先调用 Vite 开发 API真实后端失败时回退 mock
try {
const result = await webInvoke(cmd, args)
const duration = Date.now() - start
logRequest(cmd, args, duration, false)
return result
} catch (e) {
// 写操作不回退 mock直接报错避免“假成功”
if (NO_MOCK_CMDS.has(cmd)) {
logRequest(cmd, args, Date.now() - start, false)
throw e
}
console.warn(`[api] webInvoke(${cmd}) failed:`, e.message, '→ fallback mock')
const result = mockInvoke(cmd, args)
const duration = Date.now() - start
logRequest(cmd, args, duration, false)
return result
}
}
// Web 模式:通过 Vite 开发服务器的 API 端点调用真实后端
async function webInvoke(cmd, args) {
const resp = await fetch(`/__api/${cmd}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args),
})
if (!resp.ok) {
const data = await resp.json().catch(() => ({ error: `HTTP ${resp.status}` }))
throw new Error(data.error || `HTTP ${resp.status}`)
}
return resp.json()
}
// Mock 数据,方便纯浏览器开发调试