mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-19 10:09:32 +08:00
chore(release): v0.13.3
- 修复 #212 AI 消息气泡空白 - 修复 #215 HTTPS 下 WebSocket Mixed Content - 修复 #219 多实例版本检测错误 - 修复引擎切换后仪表盘无限加载 - 修复热更新假更新循环(macOS/Linux) - CI release 构建前自动同步版本号
This commit is contained in:
@@ -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