From 7b54ea583f980abf6762376b1e335372911441dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Thu, 19 Mar 2026 19:32:55 +0900 Subject: [PATCH 1/3] feat(startup): add compact startup diagnostics panel Show backend launch stages and recent logs in a collapsible startup panel so desktop users can track startup progress on top of upstream main without the loading window growing too tall. --- .../startup-shell-copy.test.mjs | 297 +++++++++- src-tauri/src/app_runtime.rs | 1 + src-tauri/src/app_types.rs | 56 +- src-tauri/src/backend/launch.rs | 5 + src-tauri/src/backend/readiness.rs | 107 +++- src-tauri/src/bridge/commands.rs | 8 + src-tauri/src/startup_task.rs | 85 ++- src-tauri/src/window/mod.rs | 1 + src-tauri/src/window/startup_panel.rs | 549 ++++++++++++++++++ ui/index.html | 431 +++++++++++++- ui/startup-copy.js | 54 ++ 11 files changed, 1583 insertions(+), 11 deletions(-) create mode 100644 src-tauri/src/window/startup_panel.rs diff --git a/scripts/prepare-resources/startup-shell-copy.test.mjs b/scripts/prepare-resources/startup-shell-copy.test.mjs index 1939aeee..cab53836 100644 --- a/scripts/prepare-resources/startup-shell-copy.test.mjs +++ b/scripts/prepare-resources/startup-shell-copy.test.mjs @@ -1,9 +1,145 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; +import vm from 'node:vm'; const startupShellPath = new URL('../../ui/index.html', import.meta.url); const startupCopyConfigPath = new URL('../../ui/startup-copy.js', import.meta.url); +const startupTaskPath = new URL('../../src-tauri/src/startup_task.rs', import.meta.url); + +const STARTUP_ELEMENT_IDS = [ + 'startup-title', + 'startup-desc', + 'startup-status', + 'startup-summary-label', + 'startup-summary-text', + 'startup-diagnostics-toggle', + 'startup-diagnostics-toggle-text', + 'startup-diagnostics', + 'startup-stage-list', + 'startup-desktop-log-label', + 'startup-desktop-log-lines', + 'startup-backend-log-label', + 'startup-backend-log-lines', +]; + +class FakeElement { + constructor(id) { + this.id = id; + this.textContent = ''; + this.title = ''; + this.hidden = false; + this.className = ''; + this.dataset = {}; + this.children = []; + this.attributes = new Map(); + this.listeners = new Map(); + } + + append(child) { + this.children.push(child); + } + + replaceChildren(...children) { + this.children = children; + } + + setAttribute(name, value) { + this.attributes.set(name, String(value)); + } + + getAttribute(name) { + return this.attributes.get(name); + } + + addEventListener(type, listener) { + const listeners = this.listeners.get(type) || []; + listeners.push(listener); + this.listeners.set(type, listeners); + } +} + +class FakeDocument { + constructor(ids) { + this.elements = new Map(ids.map((id) => [id, new FakeElement(id)])); + } + + getElementById(id) { + return this.elements.get(id) || null; + } + + createElement(tagName) { + return new FakeElement(tagName); + } +} + +const extractInlineStartupScript = (source) => { + const match = source.match(/ @@ -150,11 +367,44 @@

const title = document.getElementById("startup-title"); const desc = document.getElementById("startup-desc"); const status = document.getElementById("startup-status"); - if (!title || !desc || !status) return; + const summaryLabel = document.getElementById("startup-summary-label"); + const summaryText = document.getElementById("startup-summary-text"); + const diagnosticsToggle = document.getElementById("startup-diagnostics-toggle"); + const diagnosticsToggleText = document.getElementById( + "startup-diagnostics-toggle-text" + ); + const diagnosticsPanel = document.getElementById("startup-diagnostics"); + const stageList = document.getElementById("startup-stage-list"); + const desktopLogLabel = document.getElementById("startup-desktop-log-label"); + const desktopLogLines = document.getElementById("startup-desktop-log-lines"); + const backendLogLabel = document.getElementById("startup-backend-log-label"); + const backendLogLines = document.getElementById("startup-backend-log-lines"); + if ( + !title || + !desc || + !status || + !summaryLabel || + !summaryText || + !diagnosticsToggle || + !diagnosticsToggleText || + !diagnosticsPanel || + !stageList || + !desktopLogLabel || + !desktopLogLines || + !backendLogLabel || + !backendLogLines + ) { + return; + } if (!window.astrbot || !window.astrbot.startupShell) return; const startupShell = window.astrbot.startupShell; const { STARTUP_MODES, STARTUP_COPY } = startupShell; + const { + STARTUP_DIAGNOSTICS_COPY, + STARTUP_PANEL_COMMANDS, + STARTUP_PANEL_POLL_INTERVAL_MS, + } = startupShell; const resolveLocaleKey = () => { const locale = @@ -165,23 +415,198 @@

}; const localeKey = resolveLocaleKey(); + if (document.documentElement) { + document.documentElement.lang = localeKey === "en" ? "en" : "zh-CN"; + } const resolveStartupCopy = (mode) => { const languageCopy = STARTUP_COPY[localeKey] || STARTUP_COPY.zh; return languageCopy[mode] || languageCopy[STARTUP_MODES.LOADING]; }; + const diagnosticsCopy = + STARTUP_DIAGNOSTICS_COPY[localeKey] || STARTUP_DIAGNOSTICS_COPY.zh; + const snapshotCommand = + typeof STARTUP_PANEL_COMMANDS?.GET_SNAPSHOT === "string" + ? STARTUP_PANEL_COMMANDS.GET_SNAPSHOT + : null; + const pollIntervalMs = + Number.isFinite(STARTUP_PANEL_POLL_INTERVAL_MS) && + STARTUP_PANEL_POLL_INTERVAL_MS >= 400 + ? STARTUP_PANEL_POLL_INTERVAL_MS + : 1500; + const invoke = + typeof window.__TAURI_INTERNALS__?.invoke === "function" + ? window.__TAURI_INTERNALS__.invoke + : typeof window.__TAURI__?.core?.invoke === "function" + ? window.__TAURI__.core.invoke.bind(window.__TAURI__.core) + : null; + + let latestSnapshot = null; + let diagnosticsExpanded = false; + let pollInFlight = false; + let pollTimer = null; + + const normalizeStatusValue = (value, fallback) => + typeof value === "string" && value.trim() ? value.trim() : fallback; + + const setStatusText = (value) => { + const nextValue = normalizeStatusValue(value, summaryText.textContent); + if (status.textContent === nextValue) return; + status.textContent = nextValue; + }; + + const setSummaryText = (value) => { + const nextValue = + normalizeStatusValue(value, status.textContent); + summaryText.textContent = nextValue; + summaryText.title = nextValue; + }; + + const setDiagnosticsExpanded = (expanded) => { + diagnosticsExpanded = Boolean(expanded) && !!latestSnapshot; + diagnosticsPanel.hidden = !diagnosticsExpanded; + diagnosticsToggle.setAttribute( + "aria-expanded", + diagnosticsExpanded ? "true" : "false" + ); + diagnosticsToggleText.textContent = diagnosticsExpanded + ? diagnosticsCopy.hideDetails + : diagnosticsCopy.showDetails; + }; + + const renderStageItems = (items) => { + stageList.replaceChildren(); + const nextItems = Array.isArray(items) ? items.slice(0, 4) : []; + for (const item of nextItems) { + const chip = document.createElement("li"); + const localizedLabel = + diagnosticsCopy.stageLabels[ + typeof item?.key === "string" ? item.key : "" + ]; + chip.className = "startup-stage-chip"; + chip.dataset.done = item?.done === true ? "true" : "false"; + chip.dataset.active = item?.active === true ? "true" : "false"; + chip.textContent = localizedLabel || item?.label || "-"; + stageList.append(chip); + } + }; + + const renderLogLines = (container, lines) => { + container.replaceChildren(); + const nextLines = Array.isArray(lines) + ? lines.filter((line) => typeof line === "string" && line.trim()).slice(-8) + : []; + + if (nextLines.length === 0) { + const line = document.createElement("li"); + line.className = "startup-log-line is-empty"; + line.textContent = diagnosticsCopy.emptyLogLabel; + container.append(line); + return; + } + + for (const entry of nextLines) { + const line = document.createElement("li"); + line.className = "startup-log-line"; + line.textContent = entry; + container.append(line); + } + }; + + const resolveSnapshotSummary = (snapshot) => { + const localizedSummary = + diagnosticsCopy.stageSummaries?.[ + typeof snapshot?.stage === "string" ? snapshot.stage : "" + ]; + const snapshotSummary = + typeof snapshot?.summary === "string" && snapshot.summary.trim() + ? snapshot.summary.trim() + : ""; + const isGenericFailureSummary = + snapshotSummary.toLowerCase() === "startup failed"; + + if (snapshot?.stage === "failed") { + if (!snapshotSummary || isGenericFailureSummary) { + return localizedSummary || status.textContent; + } + + return snapshotSummary || localizedSummary || status.textContent; + } + + return localizedSummary || snapshotSummary || status.textContent; + }; + + const applySnapshot = (snapshot) => { + if (!snapshot || typeof snapshot !== "object") return; + latestSnapshot = snapshot; + renderStageItems(snapshot.items); + renderLogLines(desktopLogLines, snapshot.desktopLog); + renderLogLines(backendLogLines, snapshot.backendLog); + setStatusText(resolveSnapshotSummary(snapshot)); + setSummaryText(resolveSnapshotSummary(snapshot)); + diagnosticsToggle.hidden = false; + setDiagnosticsExpanded(diagnosticsExpanded); + }; const applyStartupMode = (mode) => { const next = resolveStartupCopy(mode); title.textContent = next.title; desc.textContent = next.desc; - if (status.textContent === next.status) return; - status.textContent = next.status; + if (latestSnapshot) return; + + setSummaryText(next.status); + setStatusText(next.status); }; + const pollStartupPanelSnapshot = async () => { + if (typeof invoke !== "function" || !snapshotCommand || pollInFlight) return; + + pollInFlight = true; + try { + const snapshot = await invoke(snapshotCommand); + applySnapshot(snapshot); + } catch { + // Keep showing the previous static copy or last successful snapshot. + } finally { + pollInFlight = false; + } + }; + + const startSnapshotPolling = () => { + if (typeof invoke !== "function" || !snapshotCommand) return; + void pollStartupPanelSnapshot(); + pollTimer = window.setInterval(() => { + void pollStartupPanelSnapshot(); + }, pollIntervalMs); + window.addEventListener( + "pagehide", + () => { + if (pollTimer !== null) { + window.clearInterval(pollTimer); + pollTimer = null; + } + }, + { once: true } + ); + }; + + summaryLabel.textContent = diagnosticsCopy.summaryLabel; + diagnosticsToggleText.textContent = diagnosticsCopy.showDetails; + stageList.setAttribute("aria-label", diagnosticsCopy.stageListLabel); + desktopLogLabel.textContent = diagnosticsCopy.desktopLogLabel; + backendLogLabel.textContent = diagnosticsCopy.backendLogLabel; + diagnosticsToggle.hidden = true; + setDiagnosticsExpanded(false); + + diagnosticsToggle.addEventListener("click", () => { + if (!latestSnapshot) return; + setDiagnosticsExpanded(!diagnosticsExpanded); + }); + window.__astrbotSetStartupMode = (mode) => { applyStartupMode(typeof mode === "string" ? mode : STARTUP_MODES.LOADING); }; applyStartupMode(STARTUP_MODES.LOADING); + startSnapshotPolling(); })(); diff --git a/ui/startup-copy.js b/ui/startup-copy.js index 880b2351..3f4bd11a 100644 --- a/ui/startup-copy.js +++ b/ui/startup-copy.js @@ -14,9 +14,17 @@ const STARTUP_MODES = { PANEL_UPDATE: 'panel-update', }; +const STARTUP_PANEL_COMMANDS = { + GET_SNAPSHOT: 'desktop_bridge_get_startup_panel_snapshot', +}; + +const STARTUP_PANEL_POLL_INTERVAL_MS = 1500; + window.astrbot = window.astrbot || {}; window.astrbot.startupShell = deepFreeze({ STARTUP_MODES, + STARTUP_PANEL_COMMANDS, + STARTUP_PANEL_POLL_INTERVAL_MS, STARTUP_COPY: { en: { [STARTUP_MODES.LOADING]: { @@ -43,4 +51,50 @@ window.astrbot.startupShell = deepFreeze({ }, }, }, + STARTUP_DIAGNOSTICS_COPY: { + en: { + summaryLabel: 'Startup', + showDetails: 'Details', + hideDetails: 'Hide', + stageListLabel: 'Startup stages', + stageSummaries: { + resolveLaunchPlan: 'Resolving launch plan', + spawnBackend: 'Spawning backend', + tcpReachable: 'TCP ready, waiting for HTTP', + httpReady: 'Backend ready', + failed: 'Startup failed', + }, + desktopLogLabel: 'Desktop', + backendLogLabel: 'Backend', + emptyLogLabel: 'No recent lines', + stageLabels: { + plan: 'Plan', + spawn: 'Spawn', + tcp: 'TCP', + http: 'HTTP', + }, + }, + zh: { + summaryLabel: '启动', + showDetails: '详情', + hideDetails: '收起', + stageListLabel: '启动阶段', + stageSummaries: { + resolveLaunchPlan: '正在解析启动计划', + spawnBackend: '正在启动后端', + tcpReachable: 'TCP 已就绪,正在等待 HTTP', + httpReady: '后端已就绪', + failed: '启动失败', + }, + desktopLogLabel: '桌面端', + backendLogLabel: '后端', + emptyLogLabel: '暂无最近日志', + stageLabels: { + plan: '计划', + spawn: '拉起', + tcp: 'TCP', + http: 'HTTP', + }, + }, + }, }); From 9473b9f4f46f30b0fcfd410f4a7d3d86e4e15b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Thu, 19 Mar 2026 19:40:42 +0900 Subject: [PATCH 2/3] fix(startup): satisfy clippy for startup panel tests --- src-tauri/src/window/startup_panel.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/window/startup_panel.rs b/src-tauri/src/window/startup_panel.rs index 16d59484..37c76791 100644 --- a/src-tauri/src/window/startup_panel.rs +++ b/src-tauri/src/window/startup_panel.rs @@ -184,6 +184,7 @@ fn read_recent_log_tail_after(path: &Path, start_offset: u64, max_bytes: usize) read_recent_log_tail_from_reader_after(&mut file, start_offset, max_bytes).unwrap_or_default() } +#[cfg(test)] fn read_recent_log_tail_from_reader(reader: &mut R, max_bytes: usize) -> io::Result where R: Read + Seek, @@ -303,14 +304,14 @@ mod tests { .expect("non-failed stages should produce stage items"); assert_eq!(items.len(), 4); - assert_eq!(items[0].done, true); - assert_eq!(items[0].active, false); - assert_eq!(items[1].done, true); - assert_eq!(items[1].active, false); - assert_eq!(items[2].done, false); - assert_eq!(items[2].active, true); - assert_eq!(items[3].done, false); - assert_eq!(items[3].active, false); + assert!(items[0].done); + assert!(!items[0].active); + assert!(items[1].done); + assert!(!items[1].active); + assert!(!items[2].done); + assert!(items[2].active); + assert!(!items[3].done); + assert!(!items[3].active); } #[test] From 9f0d58b744c0d7a6dc5ac9a0d0a5c2514e543b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Thu, 19 Mar 2026 20:08:20 +0900 Subject: [PATCH 3/3] ci: update pnpm action for Node 24 --- .github/actions/setup-desktop-build/action.yml | 2 +- .github/workflows/build-desktop-tauri.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-desktop-build/action.yml b/.github/actions/setup-desktop-build/action.yml index a181c8a3..b6fcb24b 100644 --- a/.github/actions/setup-desktop-build/action.yml +++ b/.github/actions/setup-desktop-build/action.yml @@ -33,7 +33,7 @@ runs: node-version: ${{ inputs.node-version }} - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 with: version: ${{ inputs.pnpm-version }} diff --git a/.github/workflows/build-desktop-tauri.yml b/.github/workflows/build-desktop-tauri.yml index 392a7abf..8ff78439 100644 --- a/.github/workflows/build-desktop-tauri.yml +++ b/.github/workflows/build-desktop-tauri.yml @@ -108,7 +108,7 @@ jobs: setup-python: 'false' - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 with: version: 10.28.2