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 @@
-
v0.13.2 最新版
+
v0.13.3 最新版

下载安装

选择你的操作系统,一键下载安装

@@ -1165,11 +1165,11 @@

macOS

支持 Apple Silicon 和 Intel 芯片

@@ -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 ? `` : ''} ${t('about.downloadFromWebsite')} ${t('about.downloadFromGitHub')} @@ -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 { // 检查失败静默忽略 } diff --git a/src/pages/chat-debug.js b/src/pages/chat-debug.js index cc892d6..cbcef99 100644 --- a/src/pages/chat-debug.js +++ b/src/pages/chat-debug.js @@ -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) diff --git a/src/pages/dashboard.js b/src/pages/dashboard.js index b0b614d..176370f 100644 --- a/src/pages/dashboard.js +++ b/src/pages/dashboard.js @@ -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 { diff --git a/src/router.js b/src/router.js index 811c289..411306d 100644 --- a/src/router.js +++ b/src/router.js @@ -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) { diff --git a/src/style/chat.css b/src/style/chat.css index cd90848..71e22af 100644 --- a/src/style/chat.css +++ b/src/style/chat.css @@ -1,3 +1,8 @@ +/* AI bubble markdown layout fix: #212 */ +.msg-ai .msg-bubble .msg-text { + display: block; + color: var(--text-primary); +} /** * 聊天页面样式 * 使用 clawpanel CSS 变量