fix: wsClient.close→disconnect, model vision input, memory leaks; feat: loading skeletons, panel update check; bump v0.2.0

This commit is contained in:
晴天
2026-03-04 18:07:12 +08:00
parent 57ad84fcd3
commit 3b81a193bb
12 changed files with 169 additions and 13 deletions

View File

@@ -175,6 +175,8 @@ function mockInvoke(cmd, args) {
set_npm_registry: () => true,
test_model: ({ modelId }) => `模型 ${modelId} 连通正常 (mock)`,
list_remote_models: () => ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo', 'o3-mini', 'dall-e-3', 'text-embedding-3-small'],
patch_model_vision: () => false,
check_panel_update: () => ({ latest: '0.2.0', url: 'https://github.com/qingchencloud/clawpanel/releases' }),
write_env_file: () => true,
list_backups: () => [
{ name: 'openclaw-20260226-143000.json', size: 8542, created_at: 1740577800 },
@@ -247,6 +249,8 @@ export const api = {
checkInstallation: () => cachedInvoke('check_installation', {}, 60000),
checkNode: () => cachedInvoke('check_node', {}, 60000),
getDeployConfig: () => cachedInvoke('get_deploy_config'),
patchModelVision: () => invoke('patch_model_vision'),
checkPanelUpdate: () => invoke('check_panel_update'),
writeEnvFile: (path, config) => invoke('write_env_file', { path, config }),
// 备份管理

View File

@@ -64,7 +64,7 @@ async function boot() {
if (running) {
autoConnectWebSocket()
} else {
wsClient.close()
wsClient.disconnect()
}
})
}
@@ -86,6 +86,17 @@ async function autoConnectWebSocket() {
console.warn('[main] autoPairDevice 失败(非致命):', pairErr)
}
// 确保模型配置包含 vision 支持input: ["text", "image"]
try {
const patched = await api.patchModelVision()
if (patched) {
console.log('[main] 已为模型添加 vision 支持,重载 Gateway...')
await api.reloadGateway()
}
} catch (visionErr) {
console.warn('[main] patchModelVision 失败(非致命):', visionErr)
}
wsClient.connect(`127.0.0.1:${port}`, token)
console.log('[main] WebSocket 连接已启动')
} catch (e) {

View File

@@ -65,11 +65,26 @@ async function loadData(page) {
// 非 Tauri 环境或 API 不可用,使用 fallback
}
// 异步检查 ClawPanel 自身更新
let panelUpdateHtml = '<span style="color:var(--text-tertiary)">检查更新中...</span>'
api.checkPanelUpdate().then(info => {
const panelCard = cards.querySelector('#panel-update-meta')
if (!panelCard) return
if (info.latest && info.latest !== panelVersion && compareVersions(info.latest, panelVersion) > 0) {
panelCard.innerHTML = `<span style="color:var(--accent)">新版本: ${info.latest}</span> <a class="btn btn-primary btn-sm" href="${info.url}" target="_blank" rel="noopener" style="padding:2px 8px;font-size:var(--font-size-xs)">下载更新</a>`
} else {
panelCard.innerHTML = '<span style="color:var(--success)">已是最新</span>'
}
}).catch(() => {
const panelCard = cards.querySelector('#panel-update-meta')
if (panelCard) panelCard.innerHTML = '<span style="color:var(--text-tertiary)">检查更新失败</span>'
})
cards.innerHTML = `
<div class="stat-card">
<div class="stat-card-header"><span class="stat-card-label">ClawPanel</span></div>
<div class="stat-card-value">${panelVersion}</div>
<div class="stat-card-meta">Tauri v2 桌面应用</div>
<div class="stat-card-meta" id="panel-update-meta" style="display:flex;align-items:center;gap:8px">${panelUpdateHtml}</div>
</div>
<div class="stat-card">
<div class="stat-card-header"><span class="stat-card-label">OpenClaw · ${version.source === 'official' ? '官方版' : '汉化版'}</span></div>
@@ -114,6 +129,18 @@ async function loadData(page) {
}
}
function compareVersions(a, b) {
const pa = a.split('.').map(Number)
const pb = b.split('.').map(Number)
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const na = pa[i] || 0
const nb = pb[i] || 0
if (na > nb) return 1
if (na < nb) return -1
}
return 0
}
function renderCommunity(page) {
const el = page.querySelector('#community-section')
el.innerHTML = `

View File

@@ -27,12 +27,12 @@ export async function render() {
<div id="cftunnel-card" class="config-section">
<div class="config-section-title">cftunnel 内网穿透</div>
<div class="form-hint" style="margin-bottom:var(--space-md)">通过 Cloudflare Tunnel 将本地服务暴露到公网,无需公网 IP 和端口映射。</div>
<div id="cftunnel-content"></div>
<div id="cftunnel-content"><div class="stat-card loading-placeholder" style="height:64px"></div></div>
</div>
<div id="clawapp-card" class="config-section">
<div class="config-section-title">ClawApp 移动客户端</div>
<div class="form-hint" style="margin-bottom:var(--space-md)">基于 LobeChat 的 AI 对话客户端,通过 Gateway 连接模型服务。支持本地和外网访问。</div>
<div id="clawapp-content"></div>
<div id="clawapp-content"><div class="stat-card loading-placeholder" style="height:64px"></div></div>
</div>
`

View File

@@ -13,7 +13,11 @@ export async function render() {
<h1 class="page-title">Gateway 配置</h1>
<p class="page-desc">Gateway 是 AI 模型的统一入口,所有应用通过它来调用模型服务</p>
</div>
<div id="gateway-config"></div>
<div id="gateway-config">
<div class="config-section"><div class="stat-card loading-placeholder" style="height:80px"></div></div>
<div class="config-section"><div class="stat-card loading-placeholder" style="height:80px"></div></div>
<div class="config-section"><div class="stat-card loading-placeholder" style="height:80px"></div></div>
</div>
<div class="gw-save-bar">
<button class="btn btn-primary" id="btn-save-gw">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/></svg>

View File

@@ -33,7 +33,7 @@ export async function render() {
<input type="checkbox" id="log-autoscroll" checked> 自动滚动
</label>
</div>
<div class="log-viewer" id="log-content" style="height:calc(100vh - 280px)"></div>
<div class="log-viewer" id="log-content" style="height:calc(100vh - 280px)"><div class="stat-card loading-placeholder" style="height:16px;margin:8px 0"></div><div class="stat-card loading-placeholder" style="height:16px;margin:8px 0"></div><div class="stat-card loading-placeholder" style="height:16px;margin:8px 0"></div><div class="stat-card loading-placeholder" style="height:16px;margin:8px 0"></div></div>
`
let currentTab = 'gateway'

View File

@@ -36,7 +36,7 @@ export async function render() {
<div style="padding:0 var(--space-sm) var(--space-sm)">
<button class="btn btn-sm btn-secondary" id="btn-export-zip" style="width:100%">打包下载全部</button>
</div>
<div id="file-tree"></div>
<div id="file-tree"><div class="stat-card loading-placeholder" style="height:32px;margin:8px"></div><div class="stat-card loading-placeholder" style="height:32px;margin:8px"></div><div class="stat-card loading-placeholder" style="height:32px;margin:8px"></div></div>
</div>
<div class="memory-editor">
<div class="editor-toolbar">

View File

@@ -64,7 +64,10 @@ export async function render() {
<div style="margin-bottom:var(--space-md)">
<input class="form-input" id="model-search" placeholder="搜索模型(按 ID 或名称过滤)" style="max-width:360px">
</div>
<div id="providers-list"></div>
<div id="providers-list">
<div class="config-section"><div class="stat-card loading-placeholder" style="height:120px"></div></div>
<div class="config-section"><div class="stat-card loading-placeholder" style="height:120px"></div></div>
</div>
`
const state = { config: null, search: '', undoStack: [] }
@@ -349,6 +352,12 @@ async function undo(page, state) {
// 自动保存(防抖 300ms
let _saveTimer = null
let _batchTestAbort = null // 批量测试终止控制器
export function cleanup() {
clearTimeout(_saveTimer)
_saveTimer = null
if (_batchTestAbort) { _batchTestAbort.abort = true; _batchTestAbort = null }
}
function autoSave(state) {
clearTimeout(_saveTimer)
_saveTimer = setTimeout(() => doAutoSave(state), 300)
@@ -1074,7 +1083,7 @@ async function fetchRemoteModels(btn, page, state, providerKey) {
if (!selected.length) { toast('请至少选择一个模型', 'warning'); return }
pushUndo(state)
for (const id of selected) {
provider.models.push({ id, input: ['text'] })
provider.models.push({ id, input: ['text', 'image'] })
}
overlay.remove()
renderProviders(page, state)

View File

@@ -25,8 +25,8 @@ export async function render() {
<h1 class="page-title">服务管理</h1>
<p class="page-desc">管理 OpenClaw 服务、检查更新、配置备份</p>
</div>
<div id="version-bar"></div>
<div id="services-list"></div>
<div id="version-bar"><div class="stat-card loading-placeholder" style="height:80px;margin-bottom:var(--space-lg)"></div></div>
<div id="services-list"><div class="stat-card loading-placeholder" style="height:64px"></div></div>
<div class="config-section" id="registry-section">
<div class="config-section-title">npm 源设置</div>
<div id="registry-bar"></div>
@@ -37,7 +37,7 @@ export async function render() {
<div id="backup-actions" style="margin-bottom:var(--space-md)">
<button class="btn btn-primary btn-sm" data-action="create-backup">创建备份</button>
</div>
<div id="backup-list"></div>
<div id="backup-list"><div class="stat-card loading-placeholder" style="height:48px"></div></div>
</div>
`