diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 91a35bf..0ebdab7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -71,6 +71,12 @@ jobs:
- name: 安装前端依赖
run: npm ci
+ - name: 同步版本号到构建产物
+ shell: bash
+ run: |
+ VERSION="${TAG_NAME#v}"
+ node scripts/sync-version.js "$VERSION"
+
- name: 安装 Rust 工具链
uses: dtolnay/rust-toolchain@stable
with:
diff --git a/.gitignore b/.gitignore
index 6e14d0f..75460c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,7 @@ scripts/build-r2-archive.sh
r2-archives/
# 内部开发文档(不入公开仓)
+AGENTS.md
BLOCKING_ISSUES_REPORT.md
__clawapp-chat-ref.js
LOBSTER-LEGION-ARCHIVE.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 661e435..483695a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
+## [0.13.3] - 2026-04-16
+
+### 修复 (Fixes)
+
+- **#212** 修复聊天界面 AI 消息内容在气泡中间区域空白的问题
+- **#215** 修复 HTTPS 页面下 WebSocket 测试因写死 `ws://` 触发 Mixed Content 被拦截的问题
+- **#219** 修复多实例共存时版本号检测错误:优先通过 `openclaw status --json` 读取运行中实例版本,并调整 CLI 路径查找优先级
+- 修复引擎切换后仪表盘无限加载:给 `engine.boot()` 增加 10 秒超时,切换时清空 API in-flight 缓存
+- 修复仪表盘请求超时:将整体 15 秒超时拆分为各请求独立超时,避免单个慢接口拖垮整个页面
+- 修复热更新「假更新」循环(macOS/Linux):`check_frontend_update` 优先读取已下载的 `.version` 文件,下载后写入版本号;CI release 构建前自动同步版本号
+
+---
+
## [0.13.2] - 2026-04-13
### 新功能 (Features)
diff --git a/docs/index.html b/docs/index.html
index dd729e0..ce14ee7 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -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.13.2",
+ "softwareVersion": "0.13.3",
"author": {
"@type": "Organization",
"name": "晴辰云 QingchenCloud",
@@ -1155,7 +1155,7 @@
@@ -1165,11 +1165,11 @@
macOS
支持 Apple Silicon 和 Intel 芯片
-
+
Apple Silicon (M1/M2/M3/M4)
.dmg
-
+
Intel 芯片
.dmg
@@ -1187,15 +1187,15 @@
Windows
支持 Windows 10 及以上版本
-
+
安装程序
.exe
-
+
完整包(含 WebView2)
.exe
-
+
MSI 安装包
.msi
@@ -1206,11 +1206,11 @@
Linux
支持主流 Linux 发行版
-
+
通用版
.AppImage
-
+
Debian / Ubuntu
.deb
diff --git a/package-lock.json b/package-lock.json
index fe265ec..85128a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "clawpanel",
- "version": "0.13.2",
+ "version": "0.13.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawpanel",
- "version": "0.13.2",
+ "version": "0.13.3",
"license": "AGPL-3.0",
"dependencies": {
"@tauri-apps/api": "^2.5.0",
diff --git a/package.json b/package.json
index 626dd3e..18964aa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "clawpanel",
- "version": "0.13.2",
+ "version": "0.13.3",
"private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module",
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 43fce3e..d46766b 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -351,7 +351,7 @@ dependencies = [
[[package]]
name = "clawpanel"
-version = "0.13.2"
+version = "0.13.3"
dependencies = [
"base64 0.22.1",
"chrono",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index fe83b1f..bdbc606 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
-version = "0.13.2"
+version = "0.13.3"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]
diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs
index d0cca99..425528c 100644
--- a/src-tauri/src/commands/config.rs
+++ b/src-tauri/src/commands/config.rs
@@ -1891,6 +1891,22 @@ pub fn write_mcp_config(config: Value) -> Result<(), String> {
/// macOS: 优先从 npm 包的 package.json 读取(含完整后缀),fallback 到 CLI
/// Windows/Linux: 优先读文件系统,fallback 到 CLI
async fn get_local_version() -> Option
{
+ // Fix #219: 优先从运行中的 openclaw 实例获取版本,避免多实例共存时读取到非活跃安装的版本
+ if let Ok(output) = crate::utils::openclaw_command_async()
+ .args(["status", "--json"])
+ .output()
+ .await
+ {
+ if output.status.success() {
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ if let Some(ver) = crate::commands::skills::extract_json_pub(&stdout)
+ .and_then(|v| v.get("runtimeVersion")?.as_str().map(String::from))
+ {
+ return Some(ver);
+ }
+ }
+ }
+
#[cfg(target_os = "macos")]
{
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
@@ -2638,6 +2654,17 @@ fn read_version_from_installation(cli_path: &std::path::Path) -> Option
}
}
}
+ // CLI 本体位于包目录中时(如 npm 全局安装:nvm、Homebrew 等),
+ // 直接读取同目录的 package.json(即该包自身的版本文件)
+ let own_pkg = dir.join("package.json");
+ if let Ok(content) = std::fs::read_to_string(&own_pkg) {
+ if let Some(ver) = serde_json::from_str::(&content)
+ .ok()
+ .and_then(|v| v.get("version")?.as_str().map(String::from))
+ {
+ return Some(ver);
+ }
+ }
// 根据 CLI 路径判断来源,决定 package.json 检查顺序
// 避免残留的另一来源包被优先读取
let cli_source = crate::utils::classify_cli_source(&cli_path.to_string_lossy());
diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs
index 05dc7c0..46850cf 100644
--- a/src-tauri/src/commands/hermes.rs
+++ b/src-tauri/src/commands/hermes.rs
@@ -606,7 +606,7 @@ pub fn check_hermes() -> Result {
// 提取版本号(格式可能是 "Hermes Agent v0.8.0" 或 "0.8.0")
let version = ver_raw
.split_whitespace()
- .find(|s| s.starts_with('v') || s.chars().next().map_or(false, |c| c.is_ascii_digit()))
+ .find(|s| s.starts_with('v') || s.chars().next().is_some_and(|c| c.is_ascii_digit()))
.unwrap_or(&ver_raw)
.trim_start_matches('v')
.to_string();
@@ -1888,7 +1888,7 @@ pub async fn hermes_detect_environments() -> Result {
if let Ok(ip_out) = ip_cmd {
if ip_out.status.success() {
let ip_str = String::from_utf8_lossy(&ip_out.stdout);
- let ip = ip_str.trim().split_whitespace().next().unwrap_or("").to_string();
+ let ip = ip_str.split_whitespace().next().unwrap_or("").to_string();
if !ip.is_empty() {
result["wsl2"]["ip"] = serde_json::json!(ip);
}
@@ -2004,7 +2004,7 @@ pub async fn hermes_set_gateway_url(url: Option) -> Result Result {
.map(|d| {
let secs = d.as_secs() as i64;
// Simple ISO-ish format
- let dt = chrono_simple(secs);
- dt
+ chrono_simple(secs)
})
})
.unwrap_or_default();
diff --git a/src-tauri/src/commands/update.rs b/src-tauri/src/commands/update.rs
index 55529ca..6f8ecdb 100644
--- a/src-tauri/src/commands/update.rs
+++ b/src-tauri/src/commands/update.rs
@@ -36,7 +36,15 @@ pub async fn check_frontend_update() -> Result {
.unwrap_or("")
.to_string();
- let current = env!("CARGO_PKG_VERSION");
+ // 优先读取已热更新的版本,避免 macOS/Linux 用户安装旧包后永远提示有更新
+ let current = {
+ let version_file = update_dir().join(".version");
+ std::fs::read_to_string(&version_file)
+ .ok()
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string())
+ };
// 检查最低兼容的 app 版本(前端可能依赖较新的 Rust 后端命令)
let min_app = manifest
@@ -44,8 +52,8 @@ pub async fn check_frontend_update() -> Result {
.and_then(|v| v.as_str())
.unwrap_or("0.0.0");
- let compatible = version_ge(current, min_app);
- let remote_newer = !latest.is_empty() && compatible && version_gt(&latest, current);
+ let compatible = version_ge(¤t, min_app);
+ let remote_newer = !latest.is_empty() && compatible && version_gt(&latest, ¤t);
let update_ready = remote_newer && update_dir().join("index.html").exists();
let has_update = remote_newer && !update_ready;
@@ -61,7 +69,7 @@ pub async fn check_frontend_update() -> Result {
/// 下载并解压前端更新包
#[tauri::command]
-pub async fn download_frontend_update(url: String, expected_hash: String) -> Result {
+pub async fn download_frontend_update(url: String, expected_hash: String, version: String) -> Result {
let client = super::build_http_client(std::time::Duration::from_secs(120), Some("ClawPanel"))
.map_err(|e| format!("HTTP 客户端错误: {e}"))?;
@@ -124,6 +132,11 @@ pub async fn download_frontend_update(url: String, expected_hash: String) -> Res
}
}
+ // 写入版本号文件,供下次 check_frontend_update 读取
+ if !version.is_empty() {
+ let _ = std::fs::write(dir.join(".version"), &version);
+ }
+
Ok(serde_json::json!({
"success": true,
"files": archive.len(),
diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs
index 9493993..059aef4 100644
--- a/src-tauri/src/utils.rs
+++ b/src-tauri/src/utils.rs
@@ -110,11 +110,8 @@ pub fn resolve_openclaw_cli_path() -> Option {
}
#[cfg(not(target_os = "windows"))]
{
- for candidate in common_non_windows_cli_candidates() {
- if candidate.exists() {
- return Some(candidate.to_string_lossy().to_string());
- }
- }
+ // Fix #219: 优先通过 enhanced_path 搜索:其中 nvm/volta 等版本管理器路径排在 Homebrew 前面,
+ // 与 `which openclaw` 的优先级一致,避免残留的 Homebrew 旧版本被优先检测到
let path = crate::commands::enhanced_path();
let sep = ':';
for dir in path.split(sep) {
@@ -123,6 +120,12 @@ pub fn resolve_openclaw_cli_path() -> Option {
return Some(candidate.to_string_lossy().to_string());
}
}
+ // 兜底:检查 enhanced_path 可能未覆盖到的固定路径(如 GUI 环境 PATH 受限时)
+ for candidate in common_non_windows_cli_candidates() {
+ if candidate.exists() {
+ return Some(candidate.to_string_lossy().to_string());
+ }
+ }
None
}
}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 3b3bb20..58fe27e 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "ClawPanel",
- "version": "0.13.2",
+ "version": "0.13.3",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",
diff --git a/src/components/sidebar.js b/src/components/sidebar.js
index bde764f..89311b8 100644
--- a/src/components/sidebar.js
+++ b/src/components/sidebar.js
@@ -322,6 +322,20 @@ export function renderSidebar(el) {
if (eng) {
navigate(eng.isReady() ? eng.getDefaultRoute() : eng.getSetupRoute())
}
+ }).catch(err => {
+ console.error('[sidebar] 切换引擎失败:', err)
+ toast(t('engine.switchFailed') || '引擎切换失败,请稍后重试', 'error')
+ renderSidebar(el)
+ // 恢复内容区:重新加载当前路由或显示错误占位
+ const contentEl = document.getElementById('content')
+ if (contentEl) {
+ const hash = window.location.hash.slice(1) || '/'
+ if (hash) {
+ reloadCurrentRoute()
+ } else {
+ contentEl.innerHTML = `加载失败,请刷新页面重试
`
+ }
+ }
})
}
return
diff --git a/src/engines/hermes/pages/dashboard.js b/src/engines/hermes/pages/dashboard.js
index e8b3478..7c85e5a 100644
--- a/src/engines/hermes/pages/dashboard.js
+++ b/src/engines/hermes/pages/dashboard.js
@@ -175,6 +175,15 @@ export function render() {
http://127.0.0.1:${port}
+
+
+
+ ${t('engine.dashOpenPanel')}
+
+
+
${t('engine.dashOpenPanelDesc')}
+
+
@@ -345,6 +354,8 @@ export function render() {
el.querySelectorAll('.hm-dash-link').forEach(btn => {
btn.addEventListener('click', () => { window.location.hash = '#' + btn.dataset.route })
})
+ // Open panel card
+ el.querySelector('.hm-dash-open-panel')?.addEventListener('click', () => { window.location.hash = '#/h/chat' })
// Provider presets — 点击填充 URL
el.querySelectorAll('.hm-preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
diff --git a/src/lib/engine-manager.js b/src/lib/engine-manager.js
index ce8ee98..da89477 100644
--- a/src/lib/engine-manager.js
+++ b/src/lib/engine-manager.js
@@ -2,7 +2,7 @@
* 引擎管理器
* 管理多引擎(OpenClaw / Hermes Agent / ...)的注册、切换和状态
*/
-import { api } from './tauri-api.js'
+import { api, invalidate } from './tauri-api.js'
import { registerRoute, setDefaultRoute } from '../router.js'
const _engines = {}
@@ -72,9 +72,12 @@ export async function activateEngine(id, persist = true) {
return
}
- // 清理旧引擎
- if (_activeEngine && _activeEngine.id !== id && _activeEngine.cleanup) {
- try { _activeEngine.cleanup() } catch {}
+ // 清理旧引擎 + 重置 API 缓存与 in-flight,避免旧引擎 pending 请求阻塞新引擎页面
+ if (_activeEngine && _activeEngine.id !== id) {
+ if (_activeEngine.cleanup) {
+ try { _activeEngine.cleanup() } catch {}
+ }
+ try { invalidate() } catch {}
}
_activeEngine = engine
@@ -90,8 +93,13 @@ export async function activateEngine(id, persist = true) {
// 切换时启动新引擎(检测安装状态等),初始化由 main.js 处理
if (persist && engine.boot) {
- try { await engine.boot() } catch (e) {
- console.warn('[engine-manager] boot 失败:', e)
+ try {
+ await Promise.race([
+ engine.boot(),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('engine boot timeout')), 10000))
+ ])
+ } catch (e) {
+ console.warn('[engine-manager] boot 失败或超时:', e)
}
}
diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js
index 9df8b9d..64f02cc 100644
--- a/src/lib/tauri-api.js
+++ b/src/lib/tauri-api.js
@@ -92,11 +92,15 @@ function cachedInvoke(cmd, args = {}, ttl = CACHE_TTL) {
function invalidate(...cmds) {
if (!cmds.length) {
_cache.clear()
+ _inflight.clear()
return
}
for (const [k] of _cache) {
if (cmds.some(c => k.startsWith(c))) _cache.delete(k)
}
+ for (const [k] of _inflight) {
+ if (cmds.some(c => k.startsWith(c))) _inflight.delete(k)
+ }
}
// 导出 invalidate 供外部使用
@@ -364,7 +368,7 @@ export const api = {
// 前端热更新
checkFrontendUpdate: () => invoke('check_frontend_update'),
- downloadFrontendUpdate: (url, expectedHash) => invoke('download_frontend_update', { url, expectedHash: expectedHash || '' }),
+ downloadFrontendUpdate: (url, expectedHash, version) => invoke('download_frontend_update', { url, expectedHash: expectedHash || '', version: version || '' }),
rollbackFrontendUpdate: () => invoke('rollback_frontend_update'),
getUpdateStatus: () => invoke('get_update_status'),
diff --git a/src/locales/modules/about.js b/src/locales/modules/about.js
index 80b49d5..fbde6d1 100644
--- a/src/locales/modules/about.js
+++ b/src/locales/modules/about.js
@@ -90,6 +90,12 @@ export default {
newVersionAvailable: _('发现新版本 v{version},请前往下载更新', 'New version v{version} available, please download to update', '發現新版本 v{version},請前往下載更新'),
versionMismatch: _('前端版本 v{frontend} 与应用版本 v{binary} 不一致', 'Frontend v{frontend} does not match app v{binary}', '前端版本 v{frontend} 與應用版本 v{binary} 不一致', 'フロントエンド v{frontend} とアプリ v{binary} が一致しません', '프런트엔드 v{frontend}과 앱 v{binary}이 일치하지 않습니다'),
hotUpdateDeprecated: _('热更新已弃用,请下载完整安装包以获得最佳体验', 'Hot update is deprecated, please download the full installer for the best experience', '熱更新已棄用,請下載完整安裝包以獲得最佳體驗', 'ホットアップデートは非推奨です。最高の体験のためにフルインストーラーをダウンロードしてください', '핫 업데이트는 더 이상 사용되지 않습니다. 최상의 경험을 위해 전체 설치 프로그램을 다운로드하세요'),
+ hotUpdateNow: _('立即热更新', 'Hot Update Now', '立即熱更新', '今すぐホットアップデート', '지금 핫 업데이트'),
+ hotUpdateDownloading: _('正在下载更新...', 'Downloading update...', '正在下載更新...', '更新をダウンロード中...', '업데이트 다운로드 중...'),
+ hotUpdateDone: _('更新已下载,重启后生效', 'Update downloaded, restart to apply', '更新已下載,重啟後生效', '更新をダウンロードしました。再起動して適用してください', '업데이트가 다운로드되었습니다. 적용하려면 다시 시작하세요.'),
+ hotUpdateFailed: _('更新下载失败', 'Update download failed', '更新下載失敗', '更新のダウンロードに失敗しました', '업데이트 다운로드 실패'),
+ restartApp: _('重启应用', 'Restart App', '重啟應用', 'アプリを再起動', '앱 다시 시작'),
+ restartLater: _('稍后重启', 'Restart Later', '稍後重啟', '後で再起動', '나중에 다시 시작'),
downloadFullInstaller: _('下载完整安装包', 'Download Full Installer', '下載完整安裝包', 'フルインストーラーをダウンロード', '전체 설치 프로그램 다운로드'),
upToDate: _('已是最新', 'Up to date', '', '最新です', '최신 상태', 'Đã cập nhật', 'Actualizado', 'Atualizado', 'Актуально', 'À jour', 'Aktuell'),
checkUpdateFailed: _('暂无法检查更新', 'Unable to check for updates', '暫無法檢查更新', '更新を確認できません', '업데이트 확인 실패', 'Kiểm tra cập nhật thất bại', 'Error al verificar actualizaciones', 'Falha ao verificar atualizações', 'Ошибка проверки обновлений', 'Échec de la vérification des mises à jour', 'Update-Prüfung fehlgeschlagen'),
diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js
index 2927a03..6f4292f 100644
--- a/src/locales/modules/engine.js
+++ b/src/locales/modules/engine.js
@@ -2,6 +2,7 @@ import { _ } from '../helper.js'
export default {
switchedTo: _('已切换到 {name} 模式', 'Switched to {name} mode', '已切換到 {name} 模式', '{name} モードに切り替えました', '{name} 모드로 전환됨'),
+ switchFailed: _('引擎切换失败,请稍后重试', 'Engine switch failed, please try again later', '引擎切換失敗,請稍後重試', 'エンジンの切り替えに失敗しました。後でもう一度お試しください', '엔진 전환에 실패했습니다. 잠시 후 다시 시도해 주세요'),
hermesSetupDesc: _('安装并配置 Hermes Agent', 'Install and configure Hermes Agent', '安裝並配置 Hermes Agent'),
hermesPhaseClickHint: _('点击可返回此步骤', 'Click to go back to this step', '點擊可返回此步驟', 'このステップに戻るにはクリック', '이 단계로 돌아가려면 클릭'),
hermesSetupIntro: _(
@@ -106,6 +107,8 @@ export default {
dashRestarting: _('正在重启...', 'Restarting...', '正在重啟...'),
dashQuickActions: _('快捷操作', 'Quick Actions', '快捷操作'),
dashOpenChat: _('打开对话', 'Open Chat', '開啟對話'),
+ dashOpenPanel: _('打开面板', 'Open Panel', '開啟面板'),
+ dashOpenPanelDesc: _('Hermes 对话面板', 'Hermes Chat Panel', 'Hermes 對話面板'),
dashOpenCron: _('定时任务', 'Cron Jobs', '定時任務'),
dashOpenSetup: _('重新配置', 'Reconfigure', '重新配置'),
dashNoModel: _('未配置', 'Not configured', '未配置'),
diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json
index 9249cea..1e1110f 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -110,6 +110,8 @@
"dashRestarting": "重启中…",
"dashQuickActions": "快捷操作",
"dashOpenChat": "打开对话",
+ "dashOpenPanel": "打开面板",
+ "dashOpenPanelDesc": "Hermes 对话面板",
"dashOpenCron": "定时任务",
"dashOpenSetup": "安装配置",
"dashModelConfig": "模型配置",
diff --git a/src/main.js b/src/main.js
index 4d363c3..2672b18 100644
--- a/src/main.js
+++ b/src/main.js
@@ -813,6 +813,9 @@ async function checkGlobalUpdate() {
if (dismissed === ver) return
const changelog = info.manifest?.changelog || ''
+ const canHotUpdate = isTauriRuntime()
+ && info.manifest?.downloadUrl
+ && info.manifest?.hash
banner.classList.remove('update-banner-hidden')
banner.innerHTML = `
@@ -822,6 +825,7 @@ async function checkGlobalUpdate() {
${t('about.versionAvailable', { version: ver })}
${changelog ? `
· ${changelog}` : ''}
+ ${canHotUpdate ? `