From 977dc0e339cb0bec49b107cd7c0be3b9efd2cde3 Mon Sep 17 00:00:00 2001 From: ymkiux <3255284101@qq.com> Date: Fri, 10 Apr 2026 14:14:59 +0800 Subject: [PATCH 01/17] fix: stabilize web ui startup and reduce usage load --- cli.js | 103 ++++++++-- tests/unit/compact-layout-ui.test.mjs | 5 +- tests/unit/config-tabs-ui.test.mjs | 6 +- tests/unit/run.mjs | 1 + tests/unit/session-usage-backend.test.mjs | 115 +++++++++++ tests/unit/web-run-host.test.mjs | 233 ++++++++++++++++++++++ tests/unit/web-ui-source-bundle.test.mjs | 2 + tests/unit/web-ui-startup-init.test.mjs | 93 +++++++++ web-ui/app.js | 47 ++++- web-ui/index.html | 2 +- web-ui/partials/index/layout-header.html | 2 +- web-ui/partials/index/modal-skills.html | 6 +- web-ui/partials/index/panel-docs.html | 13 +- web-ui/partials/index/panel-market.html | 28 +-- web-ui/partials/index/panel-usage.html | 2 +- web-ui/styles/navigation-panels.css | 4 +- web-ui/styles/sessions-preview.css | 10 +- 17 files changed, 621 insertions(+), 51 deletions(-) create mode 100644 tests/unit/session-usage-backend.test.mjs create mode 100644 tests/unit/web-ui-startup-init.test.mjs diff --git a/cli.js b/cli.js index 6c44a8c2..f553dce3 100644 --- a/cli.js +++ b/cli.js @@ -5775,11 +5775,21 @@ async function listSessionUsage(params = {}) { const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE)) : 200; - return await listAllSessionsData({ + const sessions = await listAllSessions({ source, limit, forceRefresh: !!params.forceRefresh }); + return Array.isArray(sessions) + ? sessions.map((item) => { + if (!item || typeof item !== 'object' || Array.isArray(item)) { + return item; + } + const normalized = { ...item }; + delete normalized.__messageCountExact; + return normalized; + }) + : []; } function listSessionPaths(params = {}) { @@ -10765,6 +10775,82 @@ const PUBLIC_WEB_UI_STATIC_ASSETS = new Set([ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) { const connections = new Set(); + const probeWebUiReadiness = (callback) => { + const payload = JSON.stringify({ action: 'status', params: {} }); + const requestOptions = { + hostname: openHost, + port, + path: '/api', + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': Buffer.byteLength(payload, 'utf-8') + } + }; + let settled = false; + const finish = (ready) => { + if (settled) return; + settled = true; + callback(ready); + }; + const req = http.request(requestOptions, (probeRes) => { + if (typeof probeRes.resume === 'function') { + probeRes.resume(); + } + probeRes.on('end', () => { + finish(probeRes.statusCode === 200); + }); + }); + req.on('error', () => finish(false)); + req.setTimeout(1000, () => { + try { req.destroy(); } catch (_) {} + finish(false); + }); + req.end(payload, 'utf-8'); + }; + const openBrowserAfterReady = (url) => { + const maxAttempts = 40; + const retryDelayMs = 150; + let finished = false; + + const finish = (ready) => { + if (finished) return; + finished = true; + if (!ready) { + console.warn('! Web UI 就绪探测超时,未自动打开浏览器,请手动访问:', url); + return; + } + + const platform = process.platform; + let command; + if (platform === 'win32') { + command = `start \"\" \"${url}\"`; + } else if (platform === 'darwin') { + command = `open \"${url}\"`; + } else { + command = `xdg-open \"${url}\"`; + } + + exec(command, (error) => { + if (error) console.warn('无法自动打开浏览器,请手动访问:', url); + }); + }; + const scheduleProbe = (attempt) => { + probeWebUiReadiness((ready) => { + if (ready) { + finish(true); + return; + } + if (attempt >= maxAttempts) { + finish(false); + return; + } + setTimeout(() => scheduleProbe(attempt + 1), retryDelayMs); + }); + }; + + scheduleProbe(1); + }; const writeWebUiAssetError = (res, requestPath, error) => { const message = error && error.message ? error.message : String(error); console.error(`! Web UI 资源读取失败 [${requestPath}]:`, message); @@ -11373,21 +11459,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser } if (!process.env.CODEXMATE_NO_BROWSER && openBrowser) { - const platform = process.platform; - let command; const url = openUrl; - - if (platform === 'win32') { - command = `start \"\" \"${url}\"`; - } else if (platform === 'darwin') { - command = `open \"${url}\"`; - } else { - command = `xdg-open \"${url}\"`; - } - - exec(command, (error) => { - if (error) console.warn('无法自动打开浏览器,请手动访问:', url); - }); + openBrowserAfterReady(url); } }); diff --git a/tests/unit/compact-layout-ui.test.mjs b/tests/unit/compact-layout-ui.test.mjs index 3a68a0b2..da0a662d 100644 --- a/tests/unit/compact-layout-ui.test.mjs +++ b/tests/unit/compact-layout-ui.test.mjs @@ -55,10 +55,13 @@ test('styles keep desktop layout wide and session history readable on large scre assert.match(styles, /\.main-panel-topbar\s*\{[\s\S]*position:\s*sticky;[\s\S]*top:\s*0;/); assert.match(styles, /\.side-item-meta\s*\{[\s\S]*display:\s*flex;[\s\S]*opacity:\s*1;/); assert.match(styles, /\.brand-logo\s*\{[\s\S]*width:\s*38px;[\s\S]*height:\s*38px;/); - assert.match(styles, /\.content-wrapper\s*\{[\s\S]*max-width:\s*1280px;/); + assert.match(styles, /\.content-wrapper\s*\{[\s\S]*width:\s*min\(100%,\s*1480px\);[\s\S]*max-width:\s*none;/); + assert.match(styles, /\.mode-content\s*\{[\s\S]*width:\s*100%;/); assert.match(styles, /\.trash-item-actions\s*\{[\s\S]*grid-template-columns:\s*repeat\(2,\s*minmax\(116px,\s*116px\)\);/); assert.match(styles, /\.trash-item-actions\s+\.btn-mini\s*\{[\s\S]*height:\s*38px;[\s\S]*min-height:\s*38px;[\s\S]*white-space:\s*nowrap;/); assert.match(styles, /\.session-layout\s*\{[\s\S]*grid-template-columns:\s*minmax\(260px,\s*360px\)\s*minmax\(0,\s*1fr\);/); + assert.match(styles, /\.session-preview-scroll\s*\{[\s\S]*padding-right:\s*52px;/); + assert.match(styles, /\.session-timeline\s*\{[\s\S]*right:\s*4px;[\s\S]*width:\s*44px;/); assert.match(styles, /\.session-item\s*\{[\s\S]*min-height:\s*102px;/); const html = readBundledWebUiHtml(); diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 73a04672..62c00aa0 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -28,7 +28,8 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /isProviderConfigMode/); assert.match(html, /provider-fast-switch-select/); assert.match(html, /forceCompactLayout/); - assert.match(html, / + diff --git a/web-ui/partials/index/layout-header.html b/web-ui/partials/index/layout-header.html index 1ab4ae6a..46f6a5c0 100644 --- a/web-ui/partials/index/layout-header.html +++ b/web-ui/partials/index/layout-header.html @@ -233,7 +233,7 @@
{{ mainTab === 'config' ? '管理 Codex、Claude Code 与 OpenClaw 的本地配置、模型与运行参数。' : (mainTab === 'sessions' ? '浏览、筛选、导出与整理本地会话记录。' : (mainTab === 'usage' ? '查看近 7 / 30 天的会话、消息与来源趋势。' : (mainTab === 'market' ? '整理安装目标、导入来源与分发入口。' : (mainTab === 'docs' ? '把 CLI 安装、升级、卸载命令与排障提示集中放进正文文档页,不再依赖悬浮入口或模态框。' : '管理下载、数据目录、回收站与全局行为。')))) }}
+{{ mainTab === 'config' ? '管理本地配置与模型。' : (mainTab === 'sessions' ? '浏览与导出会话。' : (mainTab === 'usage' ? '查看近 7 / 30 天用量。' : (mainTab === 'market' ? '管理本地 Skills。' : (mainTab === 'docs' ? '查看 CLI 安装命令与排障。' : '管理下载、目录与回收站。')))) }}