mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-28 03:01:54 +08:00
fix: 修复功能空壳问题 + 新增模型测试
- 服务管理:动态扫描 LaunchAgents plist,不再硬编码 4 个服务 - 服务启停:检查 launchctl 执行结果,失败时返回 stderr - 配置保存:Gateway/模型配置保存后自动重载 Gateway 服务使配置生效 - 模型测试:新增 test_model 命令,向 provider 发送 chat completion 验证连通性 - 新增 reqwest 依赖用于 HTTP 请求
This commit is contained in:
302
src-tauri/Cargo.lock
generated
302
src-tauri/Cargo.lock
generated
@@ -294,6 +294,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
@@ -314,6 +320,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -1010,8 +1017,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1021,9 +1030,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1298,6 +1309,23 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -1725,6 +1753,12 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -2505,6 +2539,61 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
@@ -2545,6 +2634,16 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
@@ -2565,6 +2664,16 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
@@ -2583,6 +2692,15 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
@@ -2676,6 +2794,44 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.2"
|
||||
@@ -2710,6 +2866,26 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -2719,12 +2895,53 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -2914,6 +3131,18 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.17.0"
|
||||
@@ -3161,6 +3390,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -3314,7 +3549,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"reqwest 0.13.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -3629,6 +3864,21 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.49.0"
|
||||
@@ -3643,6 +3893,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
@@ -3915,6 +4175,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -4161,6 +4427,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webkit2gtk"
|
||||
version = "2.0.2"
|
||||
@@ -4205,6 +4481,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
@@ -4435,6 +4720,15 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
@@ -4918,6 +5212,12 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
|
||||
@@ -18,3 +18,4 @@ serde_json = "1"
|
||||
dirs = "6"
|
||||
chrono = "0.4"
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||
|
||||
@@ -200,3 +200,107 @@ pub fn delete_backup(name: String) -> Result<(), String> {
|
||||
fs::remove_file(&path)
|
||||
.map_err(|e| format!("删除失败: {e}"))
|
||||
}
|
||||
|
||||
/// 重载 Gateway 服务(unload + load plist)
|
||||
#[tauri::command]
|
||||
pub fn reload_gateway() -> Result<String, String> {
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
let plist = format!(
|
||||
"{}/Library/LaunchAgents/ai.openclaw.gateway.plist",
|
||||
home.display()
|
||||
);
|
||||
|
||||
if !std::path::Path::new(&plist).exists() {
|
||||
return Err("Gateway plist 不存在".into());
|
||||
}
|
||||
|
||||
// 先 unload,忽略错误
|
||||
let _ = std::process::Command::new("launchctl")
|
||||
.args(["unload", &plist])
|
||||
.output();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
|
||||
let output = std::process::Command::new("launchctl")
|
||||
.args(["load", &plist])
|
||||
.output()
|
||||
.map_err(|e| format!("重载 Gateway 失败: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.trim().is_empty() {
|
||||
return Err(format!("重载 Gateway 失败: {stderr}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok("Gateway 已重载".into())
|
||||
}
|
||||
|
||||
/// 测试模型连通性:向 provider 发送一个简单的 chat completion 请求
|
||||
#[tauri::command]
|
||||
pub async fn test_model(
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
model_id: String,
|
||||
) -> Result<String, String> {
|
||||
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
|
||||
|
||||
let body = serde_json::json!({
|
||||
"model": model_id,
|
||||
"messages": [{"role": "user", "content": "Hi"}],
|
||||
"max_tokens": 16,
|
||||
"stream": false
|
||||
});
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?;
|
||||
|
||||
let mut req = client.post(&url).json(&body);
|
||||
if !api_key.is_empty() {
|
||||
req = req.header("Authorization", format!("Bearer {api_key}"));
|
||||
}
|
||||
|
||||
let resp = req.send().await.map_err(|e| {
|
||||
if e.is_timeout() {
|
||||
"请求超时 (30s)".to_string()
|
||||
} else if e.is_connect() {
|
||||
format!("连接失败: {e}")
|
||||
} else {
|
||||
format!("请求失败: {e}")
|
||||
}
|
||||
})?;
|
||||
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
|
||||
if !status.is_success() {
|
||||
// 尝试提取错误信息
|
||||
let msg = serde_json::from_str::<serde_json::Value>(&text)
|
||||
.ok()
|
||||
.and_then(|v| {
|
||||
v.get("error")
|
||||
.and_then(|e| e.get("message"))
|
||||
.and_then(|m| m.as_str())
|
||||
.map(String::from)
|
||||
})
|
||||
.unwrap_or_else(|| format!("HTTP {status}"));
|
||||
return Err(msg);
|
||||
}
|
||||
|
||||
// 提取回复内容
|
||||
let reply = serde_json::from_str::<serde_json::Value>(&text)
|
||||
.ok()
|
||||
.and_then(|v| {
|
||||
v.get("choices")
|
||||
.and_then(|c| c.get(0))
|
||||
.and_then(|c| c.get("message"))
|
||||
.and_then(|m| m.get("content"))
|
||||
.and_then(|c| c.as_str())
|
||||
.map(String::from)
|
||||
})
|
||||
.unwrap_or_else(|| "(无回复内容)".into());
|
||||
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
@@ -1,50 +1,46 @@
|
||||
/// 服务管理命令 (macOS launchd)
|
||||
/// 动态扫描 ~/Library/LaunchAgents/ 下的 openclaw/cftunnel 相关 plist
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::models::types::ServiceStatus;
|
||||
|
||||
const SERVICES: &[(&str, &str)] = &[
|
||||
("ai.openclaw.gateway", "OpenClaw Gateway"),
|
||||
("com.openclaw.guardian.watch", "健康监控 (60s)"),
|
||||
("com.openclaw.guardian.backup", "配置备份 (3600s)"),
|
||||
("com.openclaw.watchdog", "看门狗 (120s)"),
|
||||
];
|
||||
/// 友好名称映射
|
||||
fn description_map() -> HashMap<&'static str, &'static str> {
|
||||
HashMap::from([
|
||||
("ai.openclaw.gateway", "OpenClaw Gateway"),
|
||||
("com.openclaw.guardian.watch", "健康监控 (60s)"),
|
||||
("com.openclaw.guardian.backup", "配置备份 (3600s)"),
|
||||
("com.openclaw.watchdog", "看门狗 (120s)"),
|
||||
("com.openclaw.webhook-router", "Webhook 路由"),
|
||||
("com.openclaw.webhook-tunnel", "Webhook SSH 隧道"),
|
||||
("com.openclaw.cf-tunnel", "Cloudflare Tunnel (旧)"),
|
||||
("com.cftunnel.cloudflared", "cftunnel 隧道服务"),
|
||||
("actions.runner.2221186349-qingchen.openclaw-mac", "GitHub Actions Runner"),
|
||||
])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_services_status() -> Result<Vec<ServiceStatus>, String> {
|
||||
let output = Command::new("launchctl")
|
||||
.arg("list")
|
||||
.output()
|
||||
.map_err(|e| format!("执行 launchctl 失败: {e}"))?;
|
||||
/// 动态扫描 LaunchAgents 目录,找出所有 openclaw/cftunnel 相关 plist
|
||||
fn scan_plist_labels() -> Vec<String> {
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
let agents_dir = home.join("Library/LaunchAgents");
|
||||
let mut labels = Vec::new();
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (label, desc) in SERVICES {
|
||||
let mut status = ServiceStatus {
|
||||
label: label.to_string(),
|
||||
pid: None,
|
||||
running: false,
|
||||
description: desc.to_string(),
|
||||
};
|
||||
|
||||
// 解析 launchctl list 输出: PID\tStatus\tLabel
|
||||
for line in stdout.lines() {
|
||||
if line.contains(label) {
|
||||
let parts: Vec<&str> = line.split('\t').collect();
|
||||
if parts.len() >= 3 {
|
||||
if let Ok(pid) = parts[0].trim().parse::<u32>() {
|
||||
status.pid = Some(pid);
|
||||
status.running = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
if let Ok(entries) = fs::read_dir(&agents_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if (name.contains("openclaw") || name.contains("cftunnel"))
|
||||
&& name.ends_with(".plist")
|
||||
{
|
||||
// 文件名去掉 .plist 就是 label
|
||||
let label = name.trim_end_matches(".plist").to_string();
|
||||
labels.push(label);
|
||||
}
|
||||
}
|
||||
results.push(status);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
labels.sort();
|
||||
labels
|
||||
}
|
||||
|
||||
fn plist_path(label: &str) -> String {
|
||||
@@ -56,36 +52,100 @@ fn plist_path(label: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_services_status() -> Result<Vec<ServiceStatus>, String> {
|
||||
let output = Command::new("launchctl")
|
||||
.arg("list")
|
||||
.output()
|
||||
.map_err(|e| format!("执行 launchctl 失败: {e}"))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let labels = scan_plist_labels();
|
||||
let desc_map = description_map();
|
||||
let mut results = Vec::new();
|
||||
|
||||
for label in &labels {
|
||||
let mut status = ServiceStatus {
|
||||
label: label.clone(),
|
||||
pid: None,
|
||||
running: false,
|
||||
description: desc_map
|
||||
.get(label.as_str())
|
||||
.unwrap_or(&"")
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
// 解析 launchctl list 输出: PID\tStatus\tLabel
|
||||
for line in stdout.lines() {
|
||||
let parts: Vec<&str> = line.split('\t').collect();
|
||||
if parts.len() >= 3 && parts[2] == label {
|
||||
if let Ok(pid) = parts[0].trim().parse::<u32>() {
|
||||
status.pid = Some(pid);
|
||||
status.running = true;
|
||||
}
|
||||
// PID 为 "-" 但 label 存在于 launchctl list 中 → 已加载但未运行
|
||||
break;
|
||||
}
|
||||
}
|
||||
results.push(status);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_service(label: String) -> Result<(), String> {
|
||||
let path = plist_path(&label);
|
||||
Command::new("launchctl")
|
||||
let output = Command::new("launchctl")
|
||||
.args(["load", &path])
|
||||
.output()
|
||||
.map_err(|e| format!("启动失败: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.trim().is_empty() {
|
||||
return Err(format!("启动 {label} 失败: {stderr}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn stop_service(label: String) -> Result<(), String> {
|
||||
let path = plist_path(&label);
|
||||
Command::new("launchctl")
|
||||
let output = Command::new("launchctl")
|
||||
.args(["unload", &path])
|
||||
.output()
|
||||
.map_err(|e| format!("停止失败: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.trim().is_empty() {
|
||||
return Err(format!("停止 {label} 失败: {stderr}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn restart_service(label: String) -> Result<(), String> {
|
||||
let path = plist_path(&label);
|
||||
// 先 unload,忽略错误(可能本来就没加载)
|
||||
let _ = Command::new("launchctl")
|
||||
.args(["unload", &path])
|
||||
.output();
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
Command::new("launchctl")
|
||||
|
||||
let output = Command::new("launchctl")
|
||||
.args(["load", &path])
|
||||
.output()
|
||||
.map_err(|e| format!("重启失败: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.trim().is_empty() {
|
||||
return Err(format!("重启 {label} 失败: {stderr}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ pub fn run() {
|
||||
config::create_backup,
|
||||
config::restore_backup,
|
||||
config::delete_backup,
|
||||
config::reload_gateway,
|
||||
config::test_model,
|
||||
// 服务
|
||||
service::get_services_status,
|
||||
service::start_service,
|
||||
|
||||
@@ -18,10 +18,13 @@ async function invoke(cmd, args = {}) {
|
||||
function mockInvoke(cmd, args) {
|
||||
const mocks = {
|
||||
get_services_status: () => [
|
||||
{ label: 'ai.openclaw.gateway', pid: 54284, running: true, description: 'OpenClaw Gateway' },
|
||||
{ label: 'com.openclaw.guardian.watch', pid: 54301, running: true, description: '健康监控 (60s)' },
|
||||
{ label: 'ai.openclaw.gateway', pid: null, running: false, description: 'OpenClaw Gateway' },
|
||||
{ label: 'com.cftunnel.cloudflared', pid: 35218, running: true, description: 'cftunnel 隧道服务' },
|
||||
{ label: 'com.openclaw.guardian.watch', pid: 55290, running: true, description: '健康监控 (60s)' },
|
||||
{ label: 'com.openclaw.guardian.backup', pid: null, running: false, description: '配置备份 (3600s)' },
|
||||
{ label: 'com.openclaw.watchdog', pid: 54320, running: true, description: '看门狗 (120s)' },
|
||||
{ label: 'com.openclaw.watchdog', pid: null, running: false, description: '看门狗 (120s)' },
|
||||
{ label: 'com.openclaw.webhook-router', pid: 38983, running: true, description: 'Webhook 路由' },
|
||||
{ label: 'com.openclaw.webhook-tunnel', pid: null, running: false, description: 'Webhook SSH 隧道' },
|
||||
],
|
||||
get_version_info: () => ({
|
||||
current: '2026.2.23',
|
||||
@@ -99,6 +102,8 @@ function mockInvoke(cmd, args) {
|
||||
start_service: () => true,
|
||||
stop_service: () => true,
|
||||
restart_service: () => true,
|
||||
reload_gateway: () => 'Gateway 已重载',
|
||||
test_model: ({ base_url, model_id }) => `模型 ${model_id} 连通正常 (mock)`,
|
||||
write_env_file: () => true,
|
||||
list_backups: () => [
|
||||
{ name: 'openclaw-20260226-143000.json', size: 8542, created_at: 1740577800 },
|
||||
@@ -138,6 +143,8 @@ export const api = {
|
||||
writeOpenclawConfig: (config) => invoke('write_openclaw_config', { config }),
|
||||
readMcpConfig: () => invoke('read_mcp_config'),
|
||||
writeMcpConfig: (config) => invoke('write_mcp_config', { config }),
|
||||
reloadGateway: () => invoke('reload_gateway'),
|
||||
testModel: (baseUrl, apiKey, modelId) => invoke('test_model', { base_url: baseUrl, api_key: apiKey, model_id: modelId }),
|
||||
|
||||
// 日志
|
||||
readLogTail: (logName, lines = 100) => invoke('read_log_tail', { logName, lines }),
|
||||
|
||||
@@ -122,7 +122,13 @@ async function saveConfig(page, state) {
|
||||
|
||||
try {
|
||||
await api.writeOpenclawConfig(state.config)
|
||||
toast('Gateway 配置已保存', 'success')
|
||||
toast('Gateway 配置已保存,正在重载服务...', 'info')
|
||||
try {
|
||||
await api.reloadGateway()
|
||||
toast('Gateway 已重载,配置已生效', 'success')
|
||||
} catch (e) {
|
||||
toast('配置已保存,但重载 Gateway 失败: ' + e, 'warning')
|
||||
}
|
||||
} catch (e) {
|
||||
toast('保存失败: ' + e, 'error')
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ function renderModelCards(providerKey, models, primary) {
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex-shrink:0">
|
||||
<button class="btn btn-sm btn-secondary" data-action="test-model">测试</button>
|
||||
${!isPrimary ? `<button class="btn btn-sm btn-secondary" data-action="set-primary">设为主模型</button>` : ''}
|
||||
<button class="btn btn-sm btn-secondary" data-action="edit-model">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" data-action="delete-model">删除</button>
|
||||
@@ -191,6 +192,10 @@ function bindProviderEvents(page, state) {
|
||||
renderProviders(page, state)
|
||||
renderDefaultBar(page, state)
|
||||
toast(`已设为主模型: ${full}`, 'success')
|
||||
} else if (action === 'test-model') {
|
||||
const card = btn.closest('.model-card')
|
||||
const idx = parseInt(card.dataset.index)
|
||||
testModel(btn, state, providerKey, idx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -214,7 +219,13 @@ function bindTopActions(page, state) {
|
||||
btn.textContent = '保存中...'
|
||||
try {
|
||||
await api.writeOpenclawConfig(state.config)
|
||||
toast('模型配置已保存', 'success')
|
||||
toast('模型配置已保存,正在重载 Gateway...', 'info')
|
||||
try {
|
||||
await api.reloadGateway()
|
||||
toast('Gateway 已重载,模型配置已生效', 'success')
|
||||
} catch (e) {
|
||||
toast('配置已保存,但重载 Gateway 失败: ' + e, 'warning')
|
||||
}
|
||||
} catch (e) {
|
||||
toast('保存失败: ' + e, 'error')
|
||||
} finally {
|
||||
@@ -236,7 +247,13 @@ function bindTopActions(page, state) {
|
||||
applyDefaultModel(state)
|
||||
await api.writeOpenclawConfig(state.config)
|
||||
renderDefaultBar(page, state)
|
||||
toast('默认模型已应用', 'success')
|
||||
toast('默认模型已应用,正在重载 Gateway...', 'info')
|
||||
try {
|
||||
await api.reloadGateway()
|
||||
toast('Gateway 已重载,默认模型已生效', 'success')
|
||||
} catch (e) {
|
||||
toast('配置已保存,但重载 Gateway 失败: ' + e, 'warning')
|
||||
}
|
||||
} catch (e) {
|
||||
toast('应用失败: ' + e, 'error')
|
||||
} finally {
|
||||
@@ -350,3 +367,24 @@ function editModel(page, state, providerKey, idx) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 测试模型连通性
|
||||
async function testModel(btn, state, providerKey, idx) {
|
||||
const provider = state.config.models.providers[providerKey]
|
||||
const model = provider.models[idx]
|
||||
const modelId = typeof model === 'string' ? model : model.id
|
||||
|
||||
btn.disabled = true
|
||||
const origText = btn.textContent
|
||||
btn.textContent = '测试中...'
|
||||
|
||||
try {
|
||||
const reply = await api.testModel(provider.baseUrl, provider.apiKey || '', modelId)
|
||||
toast(`${modelId} 连通正常: "${reply.slice(0, 60)}"`, 'success')
|
||||
} catch (e) {
|
||||
toast(`${modelId} 测试失败: ${e}`, 'error')
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
btn.textContent = origText
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user