修复:macOS Gateway 启动失败 Bootstrap failed: 5 (v0.4.8)

根因: plist 二进制路径过期(nvm/fnm 切版本后),launchctl 找不到程序
修复: launchctl bootstrap/kickstart 失败时回退到 CLI 直接 spawn Gateway
覆盖: start_service_impl + restart_service_impl 均有 fallback
This commit is contained in:
晴天
2026-03-06 00:25:01 +08:00
parent fe66fe0e73
commit 7eb78ea186
6 changed files with 72 additions and 8 deletions

View File

@@ -5,6 +5,12 @@
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [0.4.8] - 2026-03-06
### 修复 (Bug Fixes)
- **macOS Gateway 启动失败 (Bootstrap failed: 5)** — plist 二进制路径过期(如 nvm/fnm 切版本后)导致 `launchctl bootstrap` 报 I/O error。新增回退机制launchctl 失败时自动改用 CLI 直接启动 Gateway启动和重启均适用
## [0.4.7] - 2026-03-06
### 修复 (Bug Fixes)

View File

@@ -1,6 +1,6 @@
{
"name": "clawpanel",
"version": "0.4.7",
"version": "0.4.8",
"private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module",

2
src-tauri/Cargo.lock generated
View File

@@ -328,7 +328,7 @@ dependencies = [
[[package]]
name = "clawpanel"
version = "0.4.7"
version = "0.4.8"
dependencies = [
"base64 0.22.1",
"chrono",

View File

@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
version = "0.4.7"
version = "0.4.8"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]

View File

@@ -100,12 +100,60 @@ mod platform {
(running, pid)
}
/// launchctl 失败时的回退:直接通过 CLI spawn Gateway 进程
fn start_gateway_direct() -> Result<(), String> {
let enhanced = crate::commands::enhanced_path();
let log_dir = dirs::home_dir()
.unwrap_or_default()
.join(".openclaw")
.join("logs");
fs::create_dir_all(&log_dir).ok();
let stdout_log = fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_dir.join("gateway.log"))
.map_err(|e| format!("创建日志文件失败: {e}"))?;
let stderr_log = fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_dir.join("gateway.err.log"))
.map_err(|e| format!("创建错误日志文件失败: {e}"))?;
Command::new("openclaw")
.arg("gateway")
.env("PATH", &enhanced)
.stdin(std::process::Stdio::null())
.stdout(stdout_log)
.stderr(stderr_log)
.spawn()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
"OpenClaw CLI 未找到,请确认已安装并重启 ClawPanel。".to_string()
} else {
format!("启动 Gateway 失败: {e}")
}
})?;
// 等 Gateway 初始化
std::thread::sleep(std::time::Duration::from_secs(2));
Ok(())
}
pub fn start_service_impl(label: &str) -> Result<(), String> {
let uid = current_uid()?;
let path = plist_path(label);
let domain_target = format!("gui/{}", uid);
let service_target = format!("gui/{}/{}", uid, label);
// 先尝试 plist 文件是否存在
if !std::path::Path::new(&path).exists() {
// plist 不存在,直接用 CLI 启动
return start_gateway_direct();
}
let bootstrap_out = Command::new("launchctl")
.args(["bootstrap", &domain_target, &path])
.output()
@@ -114,7 +162,8 @@ mod platform {
if !bootstrap_out.status.success() {
let stderr = String::from_utf8_lossy(&bootstrap_out.stderr);
if !stderr.contains("already bootstrapped") && !stderr.trim().is_empty() {
return Err(format!("启动 {label} 失败: {stderr}"));
// launchctl 失败(如 plist 二进制路径过期),回退到直接启动
return start_gateway_direct();
}
}
@@ -126,7 +175,8 @@ mod platform {
if !kickstart_out.status.success() {
let stderr = String::from_utf8_lossy(&kickstart_out.stderr);
if !stderr.trim().is_empty() {
return Err(format!("kickstart {label} 失败: {stderr}"));
// kickstart 也失败,回退到直接启动
return start_gateway_direct();
}
}
@@ -161,6 +211,7 @@ mod platform {
let domain_target = format!("gui/{}", uid);
let service_target = format!("gui/{}/{}", uid, label);
// 先停
let _ = Command::new("launchctl")
.args(["bootout", &service_target])
.output();
@@ -174,6 +225,11 @@ mod platform {
std::thread::sleep(std::time::Duration::from_millis(200));
}
// plist 不存在,直接用 CLI 启动
if !std::path::Path::new(&path).exists() {
return start_gateway_direct();
}
let bootstrap_out = Command::new("launchctl")
.args(["bootstrap", &domain_target, &path])
.output()
@@ -182,7 +238,8 @@ mod platform {
if !bootstrap_out.status.success() {
let stderr = String::from_utf8_lossy(&bootstrap_out.stderr);
if !stderr.contains("already bootstrapped") && !stderr.trim().is_empty() {
return Err(format!("重启 {label} 失败 (bootstrap): {stderr}"));
// launchctl 失败,回退到直接启动
return start_gateway_direct();
}
}
@@ -194,7 +251,8 @@ mod platform {
if !kickstart_out.status.success() {
let stderr = String::from_utf8_lossy(&kickstart_out.stderr);
if !stderr.trim().is_empty() {
return Err(format!("重启 {label} 失败 (kickstart): {stderr}"));
// kickstart 也失败,回退到直接启动
return start_gateway_direct();
}
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "ClawPanel",
"version": "0.4.7",
"version": "0.4.8",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",