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(); })();