From f411386ab5dc25de912ef48e9872bd6d036f507c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Fri, 15 May 2026 21:01:27 +0800 Subject: [PATCH] fix(ui): polish docker and model configuration views Treat an unavailable local Docker socket with no containers as an optional Docker management capability instead of showing a meaningless offline default node. Improve the model configuration layout for large model/fallback lists by wrapping controls, truncating long IDs safely, capping collapsed fallback chips, and making the fallback editor responsive. Guard chat message insertion against route changes so async history/hosted output cannot insert into an unloaded chat DOM. ## Verification - node --check src/pages/models.js - node --check src/pages/services.js - node --check src/pages/chat.js - node --check src/locales/modules/services.js - npm run build --- src/locales/modules/services.js | 2 ++ src/pages/chat.js | 32 +++++++++++---------- src/pages/models.js | 49 +++++++++++++++++---------------- src/pages/services.js | 18 ++++++++++++ 4 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/locales/modules/services.js b/src/locales/modules/services.js index 8893cfa..7483365 100644 --- a/src/locales/modules/services.js +++ b/src/locales/modules/services.js @@ -73,6 +73,8 @@ export default { dockerManager: _('Docker 管理', 'Docker Manager', 'Docker 管理'), dockerManagerHint: _('管理当前 Web 后端可访问的 Docker 节点与 OpenClaw 容器。适合本机 socket,也支持填写远程 Docker API 节点。', 'Manage Docker nodes and OpenClaw containers reachable from the current Web backend. Works with the local socket and remote Docker API endpoints.', '管理目前 Web 後端可訪問的 Docker 節點與 OpenClaw 容器。適用於本機 socket,也支援遠端 Docker API 節點。'), dockerManagerUnavailable: _('当前环境没有可用的 Web 后端,Docker 管理暂不可用。现阶段桌面版打包形态还未接入 Rust docker_* 命令;如需现在使用,请运行 Web/serve 模式。', 'A Web backend is not available in the current environment, so Docker management is temporarily unavailable. The packaged desktop build does not yet provide Rust docker_* commands. Use Web/serve mode for now.', '目前環境沒有可用的 Web 後端,Docker 管理暫不可用。現階段桌面版打包形態尚未接入 Rust docker_* 命令;如需立即使用,請運行 Web/serve 模式。'), + dockerOptionalTitle: _('Docker 多实例管理未启用', 'Docker multi-instance management is not enabled', 'Docker 多實例管理未啟用'), + dockerOptionalDesc: _('这是可选能力。未安装或未启动 Docker 时无需处理;需要用 Docker 托管多个 OpenClaw 实例时,再添加本机或远程 Docker 节点即可。', 'This is optional. If Docker is not installed or running, no action is needed. Add a local or remote Docker node only when you want to host multiple OpenClaw instances with Docker.', '這是可選能力。未安裝或未啟動 Docker 時無需處理;需要用 Docker 託管多個 OpenClaw 實例時,再新增本機或遠端 Docker 節點即可。'), dockerManagerLoadFailed: _('加载 Docker 概览失败', 'Failed to load Docker overview', '載入 Docker 概覽失敗'), dockerRefresh: _('刷新 Docker', 'Refresh Docker', '重新整理 Docker'), dockerAddNode: _('添加节点', 'Add Node', '新增節點'), diff --git a/src/pages/chat.js b/src/pages/chat.js index d59048b..ccaa397 100644 --- a/src/pages/chat.js +++ b/src/pages/chat.js @@ -2213,7 +2213,7 @@ function formatFileSize(bytes) { /** 创建流式 AI 气泡 */ function createStreamBubble() { - if (!_messagesEl || !_typingEl) return null + if (!_messagesEl || !_messagesEl.isConnected || !_typingEl) return null showTyping(false) const wrap = document.createElement('div') wrap.className = 'msg msg-ai' @@ -2221,8 +2221,7 @@ function createStreamBubble() { bubble.className = 'msg-bubble' bubble.innerHTML = '' wrap.appendChild(bubble) - _messagesEl.insertBefore(wrap, _typingEl) - scrollToBottom() + insertMessageNode(wrap) return bubble } @@ -2572,6 +2571,7 @@ function extractContent(msg) { // ── DOM 操作 ── function appendUserMessage(text, attachments = [], msgTime) { + if (!_messagesEl || !_messagesEl.isConnected) return const wrap = document.createElement('div') wrap.className = 'msg msg-user' const bubble = document.createElement('div') @@ -2628,11 +2628,11 @@ function appendUserMessage(text, attachments = [], msgTime) { wrap.appendChild(bubble) wrap.appendChild(meta) - _messagesEl.insertBefore(wrap, _typingEl) - scrollToBottom() + insertMessageNode(wrap) } function appendAiMessage(text, msgTime, images, videos, audios, files, tools) { + if (!_messagesEl || !_messagesEl.isConnected) return const wrap = document.createElement('div') wrap.className = 'msg msg-ai' const bubble = document.createElement('div') @@ -2655,8 +2655,7 @@ function appendAiMessage(text, msgTime, images, videos, audios, files, tools) { wrap.appendChild(bubble) wrap.appendChild(meta) - _messagesEl.insertBefore(wrap, _typingEl) - scrollToBottom() + insertMessageNode(wrap) } /** 渲染图片到消息气泡(支持 Anthropic/OpenAI/直接格式) */ @@ -2854,10 +2853,17 @@ function showLightbox(src) { } function appendSystemMessage(text) { + if (!_messagesEl || !_messagesEl.isConnected) return const wrap = document.createElement('div') wrap.className = 'msg msg-system' wrap.textContent = text - _messagesEl.insertBefore(wrap, _typingEl) + insertMessageNode(wrap) +} + +function insertMessageNode(wrap) { + if (!_messagesEl || !_messagesEl.isConnected) return + if (_typingEl && _typingEl.parentNode === _messagesEl) _messagesEl.insertBefore(wrap, _typingEl) + else _messagesEl.appendChild(wrap) scrollToBottom() } @@ -2903,13 +2909,12 @@ function showTyping(show, hint) { function showCompactionHint(show) { let hint = _page?.querySelector('#compaction-hint') - if (show && !hint && _messagesEl) { + if (show && !hint && _messagesEl?.isConnected) { hint = document.createElement('div') hint.id = 'compaction-hint' hint.className = 'msg msg-system compaction-hint' hint.innerHTML = `🗜️ ${t('chat.compacting')}` - _messagesEl.insertBefore(hint, _typingEl) - scrollToBottom() + insertMessageNode(hint) } else if (!show && hint) { hint.remove() } @@ -3386,14 +3391,13 @@ function normalizeHostedBaseUrl(raw, apiType) { } function appendHostedOutput(text) { - if (!text || !_messagesEl) return + if (!text || !_messagesEl || !_messagesEl.isConnected) return const hostedSessionKey = getHostedBoundSessionKey() if (hostedSessionKey && _sessionKey && hostedSessionKey !== _sessionKey) return const wrap = document.createElement('div') wrap.className = 'msg msg-system msg-hosted' wrap.textContent = `[${t('chat.hostedAgent')}] ${text}` - _messagesEl.insertBefore(wrap, _typingEl) - scrollToBottom() + insertMessageNode(wrap) } // ── 页面离开清理 ── diff --git a/src/pages/models.js b/src/pages/models.js index e30ccb6..355687d 100644 --- a/src/pages/models.js +++ b/src/pages/models.js @@ -157,25 +157,28 @@ function renderDefaultBar(page, state) { const bar = page.querySelector('#default-model-bar') const primary = getCurrentPrimary(state.config) const fallbacks = state.config?.agents?.defaults?.model?.fallbacks || [] + const visibleFallbacks = fallbacks.slice(0, 8) + const overflowFallbackCount = Math.max(0, fallbacks.length - visibleFallbacks.length) const collapsed = !state.showFallbackEditor const chevron = collapsed ? '▸' : '▾' bar.innerHTML = `
-
-
- ${chevron} +
+
+ ${chevron} ${t('models.systemModelTitle')} -
- ${primary || t('models.notConfigured')} - ${t('models.nFallbacks', { count: fallbacks.length })} +
+ ${primary || t('models.notConfigured')} + ${t('models.nFallbacks', { count: fallbacks.length })}
${collapsed && fallbacks.length > 0 ? ` -
- ${fallbacks.map(f => `${f}`).join('')} +
+ ${visibleFallbacks.map(f => `${escapeHtml(f)}`).join('')} + ${overflowFallbackCount ? `+${overflowFallbackCount}` : ''}
` : ''} @@ -222,11 +225,11 @@ function renderFallbackWaterfall(state) { return `
-
+
${t('models.bestPracticeHint')}
-
+
${t('models.activeChainTitle')}
@@ -234,7 +237,7 @@ function renderFallbackWaterfall(state) {
⋮⋮ - ${i + 1}. ${f} + ${i + 1}. ${escapeHtml(f)}
@@ -263,7 +266,7 @@ function renderFallbackWaterfall(state) {
${mIds.map(mId => `
- ${mId} + ${escapeHtml(mId)}
`).join('')} @@ -509,9 +512,9 @@ function renderProviders(page, state) { const chevron = collapsed ? '▸' : '▾' return `
-
- ${chevron}${key} ${getApiTypeLabel(p.api)} · ${t('models.nModels', { count: models.length })} -
+
+ ${chevron}${key} ${getApiTypeLabel(p.api)} · ${t('models.nModels', { count: models.length })} +
@@ -520,11 +523,11 @@ function renderProviders(page, state) {
${models.length >= 2 ? ` -
+
-
+
${t('models.sort')} -
-
- ${id} +
+
+ ${escapeHtml(id)} ${isPrimary ? `${t('models.primaryModel')}` : ''} ${m.reasoning ? `${t('models.reasoning')}` : ''} ${latencyTag}
-
${meta.join(' · ') || ''}
+
${escapeHtml(meta.join(' · ')) || ''}
-
+
${!isPrimary ? `` : ''} diff --git a/src/pages/services.js b/src/pages/services.js index 32f8194..38c4ca4 100644 --- a/src/pages/services.js +++ b/src/pages/services.js @@ -209,6 +209,24 @@ async function loadDockerManager(page) { const onlineNodes = overview.filter(node => node.online).length const totalContainers = overview.reduce((sum, node) => sum + (node.containers?.length || 0), 0) const runningContainers = overview.reduce((sum, node) => sum + (node.containers?.filter?.(ct => ct.state === 'running').length || 0), 0) + const onlyUnavailableLocal = overview.length === 1 + && overview[0]?.id === 'local' + && !overview[0]?.online + && !(overview[0]?.containers || []).length + if (!overview.length || onlyUnavailableLocal) { + bar.innerHTML = ` +
+
${t('services.dockerOptionalTitle')}
+
${t('services.dockerOptionalDesc')}
+ ${onlyUnavailableLocal ? `
${escapeHtml(overview[0]?.error || '')}
` : ''} +
+ + +
+
+ ` + return + } bar.innerHTML = `