mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-14 11:17:36 +08:00
chore(release): v0.13.3
- 修复 #212 AI 消息气泡空白 - 修复 #215 HTTPS 下 WebSocket Mixed Content - 修复 #219 多实例版本检测错误 - 修复引擎切换后仪表盘无限加载 - 修复热更新假更新循环(macOS/Linux) - CI release 构建前自动同步版本号
This commit is contained in:
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -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:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
13
CHANGELOG.md
13
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)
|
||||
|
||||
@@ -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 @@
|
||||
<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.13.2 最新版</span></div>
|
||||
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.13.3 最新版</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>
|
||||
@@ -1165,11 +1165,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.13.2_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.13.3_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.13.2_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.13.3_x64.dmg" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.mac.intel">Intel 芯片</span>
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
@@ -1187,15 +1187,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.13.2_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.13.3_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.13.2_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.13.3_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.13.2_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.13.3_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.win.msi">MSI 安装包</span>
|
||||
<span class="dl-format">.msi</span>
|
||||
</a>
|
||||
@@ -1206,11 +1206,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.13.2_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.13.3_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.13.2_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.13.3_amd64.deb" target="_blank" rel="noopener">
|
||||
Debian / Ubuntu
|
||||
<span class="dl-format">.deb</span>
|
||||
</a>
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawpanel",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.3",
|
||||
"private": true,
|
||||
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
|
||||
"type": "module",
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -351,7 +351,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clawpanel"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clawpanel"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
edition = "2021"
|
||||
description = "ClawPanel - OpenClaw 可视化管理面板"
|
||||
authors = ["qingchencloud"]
|
||||
|
||||
@@ -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<String> {
|
||||
// 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<String>
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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::<serde_json::Value>(&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());
|
||||
|
||||
@@ -606,7 +606,7 @@ pub fn check_hermes() -> Result<Value, String> {
|
||||
// 提取版本号(格式可能是 "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<Value, String> {
|
||||
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<String>) -> Result<String, Strin
|
||||
};
|
||||
|
||||
// 确保 hermes 对象存在
|
||||
if !config.get("hermes").map_or(false, |v| v.is_object()) {
|
||||
if !config.get("hermes").is_some_and(|v| v.is_object()) {
|
||||
config["hermes"] = serde_json::json!({});
|
||||
}
|
||||
|
||||
@@ -2543,8 +2543,7 @@ pub async fn hermes_logs_list() -> Result<Value, String> {
|
||||
.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();
|
||||
|
||||
@@ -36,7 +36,15 @@ pub async fn check_frontend_update() -> Result<Value, String> {
|
||||
.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<Value, String> {
|
||||
.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<Value, String> {
|
||||
|
||||
/// 下载并解压前端更新包
|
||||
#[tauri::command]
|
||||
pub async fn download_frontend_update(url: String, expected_hash: String) -> Result<Value, String> {
|
||||
pub async fn download_frontend_update(url: String, expected_hash: String, version: String) -> Result<Value, String> {
|
||||
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(),
|
||||
|
||||
@@ -110,11 +110,8 @@ pub fn resolve_openclaw_cli_path() -> Option<String> {
|
||||
}
|
||||
#[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<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = `<div class="page" style="padding:32px;color:var(--error)">加载失败,请刷新页面重试</div>`
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
|
||||
@@ -175,6 +175,15 @@ export function render() {
|
||||
<div style="font-size:13px;font-weight:600;font-family:var(--font-mono, monospace)">http://127.0.0.1:${port}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card hm-dash-open-panel" style="cursor:pointer;border-left:4px solid var(--accent,#6366f1)">
|
||||
<div class="card-body" style="padding:16px">
|
||||
<div style="font-size:12px;color:var(--text-tertiary);margin-bottom:6px;display:flex;align-items:center;gap:6px">
|
||||
${t('engine.dashOpenPanel')}
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12" style="opacity:.6"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
</div>
|
||||
<div style="font-size:14px;font-weight:600">${t('engine.dashOpenPanelDesc')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型配置区 -->
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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', '未配置'),
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
"dashRestarting": "重启中…",
|
||||
"dashQuickActions": "快捷操作",
|
||||
"dashOpenChat": "打开对话",
|
||||
"dashOpenPanel": "打开面板",
|
||||
"dashOpenPanelDesc": "Hermes 对话面板",
|
||||
"dashOpenCron": "定时任务",
|
||||
"dashOpenSetup": "安装配置",
|
||||
"dashModelConfig": "模型配置",
|
||||
|
||||
32
src/main.js
32
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() {
|
||||
<span class="update-banner-ver">${t('about.versionAvailable', { version: ver })}</span>
|
||||
${changelog ? `<span class="update-banner-changelog">· ${changelog}</span>` : ''}
|
||||
</div>
|
||||
${canHotUpdate ? `<button class="btn btn-sm btn-primary" id="btn-hot-update">${t('about.hotUpdateNow')}</button>` : ''}
|
||||
<a class="btn btn-sm" href="https://claw.qt.cool" target="_blank" rel="noopener">${t('about.downloadFromWebsite')}</a>
|
||||
<a class="btn btn-sm" href="https://github.com/qingchencloud/clawpanel/releases" target="_blank" rel="noopener">${t('about.downloadFromGitHub')}</a>
|
||||
<button class="update-banner-close" id="btn-update-dismiss" title="${t('about.dismissVersion')}">✕</button>
|
||||
@@ -833,6 +837,34 @@ async function checkGlobalUpdate() {
|
||||
localStorage.setItem('clawpanel_update_dismissed', ver)
|
||||
banner.classList.add('update-banner-hidden')
|
||||
})
|
||||
|
||||
// 热更新按钮
|
||||
const hotUpdateBtn = banner.querySelector('#btn-hot-update')
|
||||
if (hotUpdateBtn && canHotUpdate) {
|
||||
hotUpdateBtn.addEventListener('click', async () => {
|
||||
hotUpdateBtn.disabled = true
|
||||
hotUpdateBtn.textContent = t('about.hotUpdateDownloading')
|
||||
try {
|
||||
await api.downloadFrontendUpdate(
|
||||
info.manifest.downloadUrl,
|
||||
info.manifest.hash,
|
||||
ver
|
||||
)
|
||||
hotUpdateBtn.style.display = 'none'
|
||||
toast(t('about.hotUpdateDone'), 'success')
|
||||
// 在 banner 中插入重启按钮
|
||||
const rebootBtn = document.createElement('button')
|
||||
rebootBtn.className = 'btn btn-sm btn-primary'
|
||||
rebootBtn.textContent = t('about.restartApp')
|
||||
rebootBtn.onclick = () => api.relaunchApp().catch(() => {})
|
||||
banner.querySelector('.update-banner-text').after(rebootBtn)
|
||||
} catch (err) {
|
||||
hotUpdateBtn.disabled = false
|
||||
hotUpdateBtn.textContent = t('about.hotUpdateNow')
|
||||
toast(t('about.hotUpdateFailed') + ': ' + (err.message || err), 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// 检查失败静默忽略
|
||||
}
|
||||
|
||||
@@ -388,7 +388,8 @@ async function testWebSocketAdv(page) {
|
||||
const rawToken = config?.gateway?.auth?.token
|
||||
const token = (typeof rawToken === 'string') ? rawToken : ''
|
||||
const wsHost = isTauriRuntime() ? `127.0.0.1:${port}` : location.host
|
||||
const url = `ws://${wsHost}/ws?token=${encodeURIComponent(token)}`
|
||||
const wsScheme = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const url = `${wsScheme}://${wsHost}/ws?token=${encodeURIComponent(token)}`
|
||||
log(`→ ${url}`)
|
||||
|
||||
const ws = new WebSocket(url)
|
||||
|
||||
@@ -126,19 +126,20 @@ async function _loadDashboardDataInner(page, fullRefresh) {
|
||||
promise,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error(`超时(${ms/1000}s)`)), ms))
|
||||
])
|
||||
const coreP = withTimeout(Promise.allSettled([
|
||||
api.getServicesStatus(),
|
||||
api.readOpenclawConfig(),
|
||||
// 每个请求独立超时:避免单个慢请求拖垮整体渲染
|
||||
const coreP = Promise.allSettled([
|
||||
withTimeout(api.getServicesStatus(), 12000),
|
||||
withTimeout(api.readOpenclawConfig(), 5000),
|
||||
// 版本信息:首次加载或手动刷新时才查询(避免 ARM 设备上频繁查 npm registry)
|
||||
(!_dashboardInitialized || fullRefresh || !_dashboardVersionCache) ? api.getVersionInfo() : Promise.resolve(_dashboardVersionCache),
|
||||
api.readPanelConfig(),
|
||||
]), 15000)
|
||||
const secondaryP = withTimeout(Promise.allSettled([
|
||||
api.listAgents(),
|
||||
api.readMcpConfig(),
|
||||
api.listBackups(),
|
||||
api.listConfiguredPlatforms().catch(() => []),
|
||||
]), 15000).catch(() => [{ status: 'rejected' }, { status: 'rejected' }, { status: 'rejected' }, { status: 'rejected' }])
|
||||
(!_dashboardInitialized || fullRefresh || !_dashboardVersionCache) ? withTimeout(api.getVersionInfo(), 8000) : Promise.resolve(_dashboardVersionCache),
|
||||
withTimeout(api.readPanelConfig(), 5000),
|
||||
])
|
||||
const secondaryP = Promise.allSettled([
|
||||
withTimeout(api.listAgents(), 10000),
|
||||
withTimeout(api.readMcpConfig(), 10000),
|
||||
withTimeout(api.listBackups(), 10000),
|
||||
withTimeout(api.listConfiguredPlatforms(), 10000).catch(() => []),
|
||||
])
|
||||
const logsP = api.readLogTail('gateway', 20).catch(() => '')
|
||||
|
||||
// 第一波:服务状态 + 配置 + 版本 → 立即渲染统计卡片
|
||||
@@ -200,7 +201,7 @@ async function _loadDashboardDataInner(page, fullRefresh) {
|
||||
if (shouldLoadStatusSummary) {
|
||||
try {
|
||||
statusSummary = (!_dashboardInitialized || fullRefresh || !_dashboardStatusSummaryCache)
|
||||
? await withTimeout(api.getStatusSummary(), 15000)
|
||||
? await withTimeout(api.getStatusSummary(), 10000)
|
||||
: _dashboardStatusSummaryCache
|
||||
_dashboardStatusSummaryCache = statusSummary
|
||||
} catch {
|
||||
|
||||
@@ -19,7 +19,12 @@ export function setDefaultRoute(path) {
|
||||
}
|
||||
|
||||
export function navigate(path) {
|
||||
const current = window.location.hash.slice(1)
|
||||
window.location.hash = path
|
||||
// 如果 hash 没有实际变化,手动触发加载(引擎切换等场景兜底)
|
||||
if (current === path) {
|
||||
reloadCurrentRoute()
|
||||
}
|
||||
}
|
||||
|
||||
export function initRouter(contentEl) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/* AI bubble markdown layout fix: #212 */
|
||||
.msg-ai .msg-bubble .msg-text {
|
||||
display: block;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
/**
|
||||
* 聊天页面样式
|
||||
* 使用 clawpanel CSS 变量
|
||||
|
||||
Reference in New Issue
Block a user