mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 21:00:30 +08:00
fix: wsClient.close→disconnect, model vision input, memory leaks; feat: loading skeletons, panel update check; bump v0.2.0
This commit is contained in:
@@ -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 }),
|
||||
|
||||
// 备份管理
|
||||
|
||||
13
src/main.js
13
src/main.js
@@ -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) {
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user