fix(gateway): release 0.16.5

This commit is contained in:
晴天
2026-05-22 20:41:24 +08:00
parent 5aa4c9aa06
commit d0f0d1f6b7
11 changed files with 241 additions and 41 deletions

View File

@@ -7,6 +7,18 @@
## [未发布]
## [0.16.5] - 2026-05-22
### 修复 (Fixes)
- **兼容已有 OpenClaw 安装** — Windows 桌面端自动发现范围扩展到 npm 全局目录、standalone 目录、常见 OpenClaw / Node 安装目录、`where openclaw` 结果,并支持 `openclaw.exe` / `openclaw.js` 入口,避免 Gateway 已运行但重启时报 “openclaw CLI 未安装”
- **已有安装导入引导** — Dashboard 启动 / 重启 Gateway 遇到 CLI 未绑定时,会打开已有 OpenClaw 安装绑定弹窗,不再只显示失败提示
- **Web 模式候选路径补齐** — Web 后端 CLI 发现同步补充 `openclaw.exe` 候选,保持桌面端与 Web 端行为一致
### 测试与验证 (Testing)
- **回归验证** — 已通过 `npm run build``node --test tests\*.test.js``cargo fmt --check``cargo check``cargo clippy --all-targets -- -D warnings``git diff --check`
## [0.16.4] - 2026-05-22
### 修复 (Fixes)

View File

@@ -34,7 +34,7 @@
"description": "支持 OpenClaw 和 Hermes Agent 双引擎的多 AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。内置晴辰助手支持工具调用,晴辰云 AI 接口一键接入。支持仪表盘监控、多模型配置、Hermes Agent 对话、消息渠道管理、内置 QQ 机器人、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。支持 11 种语言。",
"url": "https://claw.qt.cool/",
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
"softwareVersion": "0.16.4",
"softwareVersion": "0.16.5",
"author": {
"@type": "Organization",
"name": "晴辰云 QingchenCloud",
@@ -1225,7 +1225,7 @@
<div class="orb orb-2" style="top:auto;bottom:-100px"></div>
<div class="container-sm" style="position:relative;z-index:10">
<div class="section-header">
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.16.4 最新版</span></div>
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.16.5 最新版</span></div>
<h2 class="reveal section-title" data-i18n="dl.title"><span class="gradient-text">下载安装</span></h2>
<p class="reveal section-desc" data-i18n="dl.desc">选择你的操作系统,一键下载安装</p>
</div>
@@ -1235,11 +1235,11 @@
<h3>macOS</h3>
<p class="dl-desc" data-i18n="dl.mac.d">支持 Apple Silicon 和 Intel 芯片</p>
<div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.4_aarch64.dmg" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.5_aarch64.dmg" target="_blank" rel="noopener">
Apple Silicon (M1/M2/M3/M4)
<span class="dl-format">.dmg</span>
</a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.4_x64.dmg" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.5_x64.dmg" target="_blank" rel="noopener">
<span data-i18n="dl.mac.intel">Intel 芯片</span>
<span class="dl-format">.dmg</span>
</a>
@@ -1257,15 +1257,15 @@
<h3>Windows</h3>
<p class="dl-desc" data-i18n="dl.win.d">支持 Windows 10 及以上版本</p>
<div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.4_x64-setup.exe" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.5_x64-setup.exe" target="_blank" rel="noopener">
<span data-i18n="dl.win.exe">安装程序</span>
<span class="dl-format">.exe</span>
</a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.4_x64-setup-full.exe" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.5_x64-setup-full.exe" target="_blank" rel="noopener">
<span data-i18n="dl.win.full">完整包(含 WebView2</span>
<span class="dl-format">.exe</span>
</a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.4_x64_en-US.msi" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.5_x64_en-US.msi" target="_blank" rel="noopener">
<span data-i18n="dl.win.msi">MSI 安装包</span>
<span class="dl-format">.msi</span>
</a>
@@ -1276,11 +1276,11 @@
<h3>Linux</h3>
<p class="dl-desc" data-i18n="dl.linux.d">支持主流 Linux 发行版</p>
<div class="dl-links">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.4_amd64.AppImage" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.5_amd64.AppImage" target="_blank" rel="noopener">
<span data-i18n="dl.linux.ai">通用版</span>
<span class="dl-format">.AppImage</span>
</a>
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.4_amd64.deb" target="_blank" rel="noopener">
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.16.5_amd64.deb" target="_blank" rel="noopener">
Debian / Ubuntu
<span class="dl-format">.deb</span>
</a>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "clawpanel",
"version": "0.16.4",
"version": "0.16.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawpanel",
"version": "0.16.4",
"version": "0.16.5",
"license": "AGPL-3.0",
"dependencies": {
"@tauri-apps/api": "^2.5.0",

View File

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

View File

@@ -585,6 +585,7 @@ function collectPreferredCliCandidates() {
if (!trimmed) continue
if (isWindows) {
addCliCandidate(candidates, seen, path.join(trimmed, 'openclaw.cmd'))
addCliCandidate(candidates, seen, path.join(trimmed, 'openclaw.exe'))
addCliCandidate(candidates, seen, path.join(trimmed, 'openclaw'))
} else {
addCliCandidate(candidates, seen, path.join(trimmed, 'openclaw'))

2
src-tauri/Cargo.lock generated
View File

@@ -366,7 +366,7 @@ dependencies = [
[[package]]
name = "clawpanel"
version = "0.16.4"
version = "0.16.5"
dependencies = [
"base64 0.22.1",
"chrono",

View File

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

View File

@@ -2496,12 +2496,37 @@ fn scan_all_installations(
try_add(prefix_path.join("openclaw"));
}
if let Ok(localappdata) = std::env::var("LOCALAPPDATA") {
let localappdata_path = std::path::PathBuf::from(&localappdata);
try_add(
std::path::PathBuf::from(&localappdata)
localappdata_path
.join("Programs")
.join("OpenClaw")
.join("openclaw.exe"),
);
try_add(localappdata_path.join("OpenClaw").join("openclaw.cmd"));
try_add(localappdata_path.join("OpenClaw").join("openclaw.exe"));
try_add(
localappdata_path
.join("Programs")
.join("nodejs")
.join("openclaw.cmd"),
);
try_add(
localappdata_path
.join("Programs")
.join("nodejs")
.join("openclaw.exe"),
);
try_add(
localappdata_path
.join("Programs")
.join("nodejs")
.join("node_modules")
.join("@qingchencloud")
.join("openclaw-zh")
.join("bin")
.join("openclaw.js"),
);
}
if let Ok(program_files) = std::env::var("ProgramFiles") {
try_add(
@@ -2607,6 +2632,15 @@ fn scan_all_installations(
#[cfg(target_os = "windows")]
{
try_add(base.join("openclaw.cmd"));
try_add(base.join("openclaw.exe"));
try_add(base.join("openclaw"));
try_add(
base.join("node_modules")
.join("@qingchencloud")
.join("openclaw-zh")
.join("bin")
.join("openclaw.js"),
);
}
#[cfg(not(target_os = "windows"))]
{

View File

@@ -1,6 +1,142 @@
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
fn push_unique_candidate(
candidates: &mut Vec<std::path::PathBuf>,
seen: &mut std::collections::HashSet<String>,
path: std::path::PathBuf,
) {
let key = path.to_string_lossy().replace('/', "\\").to_lowercase();
if seen.insert(key) {
candidates.push(path);
}
}
#[cfg(target_os = "windows")]
fn push_windows_cli_files(
candidates: &mut Vec<std::path::PathBuf>,
seen: &mut std::collections::HashSet<String>,
base: std::path::PathBuf,
) {
push_unique_candidate(candidates, seen, base.join("openclaw.cmd"));
push_unique_candidate(candidates, seen, base.join("openclaw.exe"));
push_unique_candidate(candidates, seen, base.join("openclaw"));
push_unique_candidate(
candidates,
seen,
base.join("node_modules")
.join("@qingchencloud")
.join("openclaw-zh")
.join("bin")
.join("openclaw.js"),
);
push_unique_candidate(
candidates,
seen,
base.join("node_modules")
.join("openclaw")
.join("bin")
.join("openclaw.js"),
);
}
#[cfg(target_os = "windows")]
fn common_windows_cli_candidates() -> Vec<std::path::PathBuf> {
let mut candidates = Vec::new();
let mut seen = std::collections::HashSet::new();
// 先按 enhanced PATH 顺序找,保持与用户命令行优先级一致。
for dir in crate::commands::enhanced_path().split(';') {
let dir = dir.trim();
if dir.is_empty() {
continue;
}
push_windows_cli_files(&mut candidates, &mut seen, std::path::PathBuf::from(dir));
}
if let Ok(appdata) = std::env::var("APPDATA") {
push_windows_cli_files(
&mut candidates,
&mut seen,
std::path::PathBuf::from(appdata).join("npm"),
);
}
if let Some(prefix) = crate::commands::windows_npm_global_prefix() {
push_windows_cli_files(&mut candidates, &mut seen, std::path::PathBuf::from(prefix));
}
for sa_dir in crate::commands::config::all_standalone_dirs() {
push_windows_cli_files(&mut candidates, &mut seen, sa_dir);
}
if let Ok(localappdata) = std::env::var("LOCALAPPDATA") {
let localappdata = std::path::PathBuf::from(localappdata);
push_windows_cli_files(
&mut candidates,
&mut seen,
localappdata.join("Programs").join("OpenClaw"),
);
push_windows_cli_files(&mut candidates, &mut seen, localappdata.join("OpenClaw"));
push_windows_cli_files(
&mut candidates,
&mut seen,
localappdata.join("Programs").join("nodejs"),
);
}
if let Ok(program_files) = std::env::var("ProgramFiles") {
let program_files = std::path::PathBuf::from(program_files);
push_windows_cli_files(&mut candidates, &mut seen, program_files.join("nodejs"));
push_windows_cli_files(&mut candidates, &mut seen, program_files.join("OpenClaw"));
}
if let Ok(program_files_x86) = std::env::var("ProgramFiles(x86)") {
push_windows_cli_files(
&mut candidates,
&mut seen,
std::path::PathBuf::from(program_files_x86).join("nodejs"),
);
}
if let Ok(profile) = std::env::var("USERPROFILE") {
push_windows_cli_files(
&mut candidates,
&mut seen,
std::path::PathBuf::from(profile).join(".openclaw-bin"),
);
}
for drive in ["C", "D", "E", "F", "G"] {
push_windows_cli_files(
&mut candidates,
&mut seen,
std::path::PathBuf::from(format!(r"{drive}:\OpenClaw")),
);
push_windows_cli_files(
&mut candidates,
&mut seen,
std::path::PathBuf::from(format!(r"{drive}:\AI\OpenClaw")),
);
}
const CREATE_NO_WINDOW: u32 = 0x08000000;
let mut where_cmd = std::process::Command::new("where");
where_cmd.arg("openclaw");
where_cmd.env("PATH", crate::commands::enhanced_path());
where_cmd.creation_flags(CREATE_NO_WINDOW);
if let Ok(output) = where_cmd.output() {
if output.status.success() {
for line in String::from_utf8_lossy(&output.stdout).lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
push_unique_candidate(
&mut candidates,
&mut seen,
std::path::PathBuf::from(trimmed),
);
}
}
}
}
candidates
}
pub fn is_rejected_cli_path(cli_path: &str) -> bool {
let lower = cli_path.replace('\\', "/").to_lowercase();
lower.contains("/.cherrystudio/") || lower.contains("cherry-studio")
@@ -41,7 +177,7 @@ fn configured_cli_candidates() -> Vec<std::path::PathBuf> {
.collect()
}
/// Windows: 在 PATH 中查找 openclaw.cmd 的完整路径
/// Windows: 在 PATH 和常见安装目录中查找 openclaw CLI 的完整路径
/// 避免通过 `cmd /c openclaw` 调用时 npm .cmd shim 中的引号导致
/// "\"node\"" is not recognized 错误
#[cfg(target_os = "windows")]
@@ -55,14 +191,9 @@ fn find_openclaw_cmd() -> Option<std::path::PathBuf> {
return Some(candidate);
}
}
let path = crate::commands::enhanced_path();
for dir in path.split(';') {
let candidate = std::path::Path::new(dir).join("openclaw.cmd");
if candidate.exists() && !is_rejected_cli_path(&candidate.to_string_lossy()) {
return Some(candidate);
}
}
None
common_windows_cli_candidates()
.into_iter()
.find(|candidate| candidate.exists() && !is_rejected_cli_path(&candidate.to_string_lossy()))
}
#[cfg(not(target_os = "windows"))]
@@ -95,14 +226,7 @@ pub fn resolve_openclaw_cli_path() -> Option<String> {
}
#[cfg(target_os = "windows")]
{
let path = crate::commands::enhanced_path();
for dir in path.split(';') {
let candidate = std::path::Path::new(dir).join("openclaw.cmd");
if candidate.exists() && !is_rejected_cli_path(&candidate.to_string_lossy()) {
return Some(candidate.to_string_lossy().to_string());
}
}
None
find_openclaw_cmd().map(|p| p.to_string_lossy().to_string())
}
#[cfg(not(target_os = "windows"))]
{
@@ -161,7 +285,15 @@ pub fn openclaw_command() -> std::process::Command {
// 优先:找到 openclaw.cmd 完整路径,用 cmd /c "完整路径" 避免引号问题
if let Some(cmd_path) = find_openclaw_cmd() {
let mut cmd = std::process::Command::new("cmd");
cmd.arg("/c").arg(cmd_path);
if cmd_path
.extension()
.and_then(|s| s.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("js"))
{
cmd.arg("/c").arg("node").arg(cmd_path);
} else {
cmd.arg("/c").arg(cmd_path);
}
cmd.env("PATH", &enhanced);
apply_openclaw_dir_env(&mut cmd);
crate::commands::apply_proxy_env(&mut cmd);
@@ -197,7 +329,15 @@ pub fn openclaw_command_async() -> tokio::process::Command {
// 优先:找到 openclaw.cmd 完整路径
if let Some(cmd_path) = find_openclaw_cmd() {
let mut cmd = tokio::process::Command::new("cmd");
cmd.arg("/c").arg(cmd_path);
if cmd_path
.extension()
.and_then(|s| s.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("js"))
{
cmd.arg("/c").arg("node").arg(cmd_path);
} else {
cmd.arg("/c").arg(cmd_path);
}
cmd.env("PATH", &enhanced);
apply_openclaw_dir_env_tokio(&mut cmd);
crate::commands::apply_proxy_env_tokio(&mut cmd);

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.16.4",
"version": "0.16.5",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",

View File

@@ -119,6 +119,22 @@ function dedupeOpenclawInstallations(list = []) {
return [...map.values()]
}
function isCliMissingError(err) {
const message = String(err?.message || err || '')
return message.includes('openclaw CLI 未安装') || message.includes('CLI 未安装')
}
async function handleGatewayStartError(page, err, fallbackText) {
if (isForeignGatewayError(err)) {
await openGatewayConflict(page, err)
return
}
toast(humanizeError(err, fallbackText), isCliMissingError(err) ? 'warning' : 'error')
if (isCliMissingError(err)) {
await showInstallationCleanup({ onRefresh: () => loadDashboardData(page, true) })
}
}
let _dashboardInitialized = false
let _dashboardVersionCache = null
let _dashboardStatusSummaryCache = null
@@ -760,8 +776,7 @@ function bindActions(page) {
toast(t('dashboard.gwStartSent'), 'success')
setTimeout(() => loadDashboardData(page), 2000)
} catch (err) {
if (isForeignGatewayError(err)) await openGatewayConflict(page, err)
else toast(t('dashboard.startFail') + ': ' + err, 'error')
await handleGatewayStartError(page, err, t('dashboard.startFail'))
}
finally { actionBtn.disabled = false; actionBtn.textContent = t('dashboard.startBtn') }
}
@@ -784,8 +799,7 @@ function bindActions(page) {
toast(t('dashboard.gwRestartSent'), 'success')
setTimeout(() => loadDashboardData(page), 3000)
} catch (err) {
if (isForeignGatewayError(err)) await openGatewayConflict(page, err)
else toast(t('dashboard.restartFail') + ': ' + err, 'error')
await handleGatewayStartError(page, err, t('dashboard.restartFail'))
}
finally { actionBtn.disabled = false; actionBtn.textContent = t('dashboard.restartBtn') }
}
@@ -798,8 +812,7 @@ function bindActions(page) {
try {
await api.restartService('ai.openclaw.gateway')
} catch (e) {
if (isForeignGatewayError(e)) await openGatewayConflict(page, e)
else toast(humanizeError(e, t('dashboard.restartFail')), 'error')
await handleGatewayStartError(page, e, t('dashboard.restartFail'))
btnRestart.disabled = false
btnRestart.classList.remove('btn-loading')
btnRestart.textContent = t('dashboard.restartGw')