feat(web): display backend version info in sidebar

This commit is contained in:
lc6464 2026-03-27 18:39:11 +08:00
parent 76cd7f8ad5
commit 18ec2631aa
No known key found for this signature in database
GPG key ID: 53C61B42FEC71D6D
7 changed files with 185 additions and 0 deletions

View file

@ -76,6 +76,9 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
// Launcher service parameters (port/public) // Launcher service parameters (port/public)
h.registerLauncherConfigRoutes(mux) h.registerLauncherConfigRoutes(mux)
// Runtime build/version metadata
h.registerVersionRoutes(mux)
// WeChat QR login flow // WeChat QR login flow
h.registerWeixinRoutes(mux) h.registerWeixinRoutes(mux)

View file

@ -0,0 +1,31 @@
package api
import (
"encoding/json"
"net/http"
"github.com/sipeed/picoclaw/pkg/config"
)
type systemVersionResponse struct {
Version string `json:"version"`
GitCommit string `json:"git_commit,omitempty"`
BuildTime string `json:"build_time,omitempty"`
GoVersion string `json:"go_version"`
}
func (h *Handler) registerVersionRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/system/version", h.handleGetVersion)
}
func (h *Handler) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
buildTime, goVer := config.FormatBuildInfo()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(systemVersionResponse{
Version: config.GetVersion(),
GitCommit: config.GitCommit,
BuildTime: buildTime,
GoVersion: goVer,
})
}

View file

@ -0,0 +1,98 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"github.com/sipeed/picoclaw/pkg/config"
)
func TestGetSystemVersion(t *testing.T) {
originalVersion := config.Version
originalGitCommit := config.GitCommit
originalBuildTime := config.BuildTime
originalGoVersion := config.GoVersion
t.Cleanup(func() {
config.Version = originalVersion
config.GitCommit = originalGitCommit
config.BuildTime = originalBuildTime
config.GoVersion = originalGoVersion
})
config.Version = "v1.2.3"
config.GitCommit = "deadbeef"
config.BuildTime = "2026-03-27T12:34:56Z"
config.GoVersion = "go1.24.1"
h := NewHandler("")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/system/version", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var got systemVersionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if got.Version != config.Version {
t.Fatalf("version = %q, want %q", got.Version, config.Version)
}
if got.GitCommit != config.GitCommit {
t.Fatalf("git_commit = %q, want %q", got.GitCommit, config.GitCommit)
}
if got.BuildTime != config.BuildTime {
t.Fatalf("build_time = %q, want %q", got.BuildTime, config.BuildTime)
}
if got.GoVersion != config.GoVersion {
t.Fatalf("go_version = %q, want %q", got.GoVersion, config.GoVersion)
}
}
func TestGetSystemVersionUsesRuntimeGoVersionFallback(t *testing.T) {
originalVersion := config.Version
originalGitCommit := config.GitCommit
originalBuildTime := config.BuildTime
originalGoVersion := config.GoVersion
t.Cleanup(func() {
config.Version = originalVersion
config.GitCommit = originalGitCommit
config.BuildTime = originalBuildTime
config.GoVersion = originalGoVersion
})
config.Version = "dev"
config.GitCommit = ""
config.BuildTime = ""
config.GoVersion = ""
h := NewHandler("")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/system/version", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var got systemVersionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if got.GoVersion != runtime.Version() {
t.Fatalf("go_version = %q, want runtime version %q", got.GoVersion, runtime.Version())
}
}

View file

@ -11,6 +11,13 @@ export interface LauncherConfig {
allowed_cidrs: string[] allowed_cidrs: string[]
} }
export interface SystemVersionInfo {
version: string
git_commit?: string
build_time?: string
go_version: string
}
async function request<T>(path: string, options?: RequestInit): Promise<T> { async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(path, options) const res = await fetch(path, options)
if (!res.ok) { if (!res.ok) {
@ -60,3 +67,7 @@ export async function setLauncherConfig(
body: JSON.stringify(payload), body: JSON.stringify(payload),
}) })
} }
export async function getSystemVersionInfo(): Promise<SystemVersionInfo> {
return request<SystemVersionInfo>("/api/system/version")
}

View file

@ -10,10 +10,12 @@ import {
IconSparkles, IconSparkles,
IconTools, IconTools,
} from "@tabler/icons-react" } from "@tabler/icons-react"
import { useQuery } from "@tanstack/react-query"
import { Link, useRouterState } from "@tanstack/react-router" import { Link, useRouterState } from "@tanstack/react-router"
import * as React from "react" import * as React from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { getSystemVersionInfo } from "@/api/system"
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
@ -27,6 +29,7 @@ import {
SidebarGroupLabel, SidebarGroupLabel,
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarFooter,
SidebarMenuItem, SidebarMenuItem,
SidebarRail, SidebarRail,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
@ -78,6 +81,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
language: (i18n.resolvedLanguage ?? i18n.language ?? "").toLowerCase(), language: (i18n.resolvedLanguage ?? i18n.language ?? "").toLowerCase(),
t, t,
}) })
const { data: versionInfo } = useQuery({
queryKey: ["system", "version"],
queryFn: getSystemVersionInfo,
staleTime: 5 * 60 * 1000,
})
const versionText = versionInfo?.version ?? t("footer.version_unknown")
const navGroups: NavGroup[] = React.useMemo(() => { const navGroups: NavGroup[] = React.useMemo(() => {
return [ return [
@ -235,6 +245,26 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</Collapsible> </Collapsible>
))} ))}
</SidebarContent> </SidebarContent>
<SidebarFooter className="border-t-border/30 group-data-[collapsible=icon]:hidden border-t px-3 py-2">
<div className="text-muted-foreground flex flex-col gap-0.5 text-[11px] leading-4">
<div className="truncate" title={versionText}>
<span className="text-foreground/80">{t("footer.version")}:</span>{" "}
{versionText}
</div>
{versionInfo?.git_commit && (
<div className="truncate" title={versionInfo.git_commit}>
<span className="text-foreground/80">{t("footer.commit")}:</span>{" "}
{versionInfo.git_commit}
</div>
)}
{versionInfo?.build_time && (
<div className="truncate" title={versionInfo.build_time}>
<span className="text-foreground/80">{t("footer.build")}:</span>{" "}
{versionInfo.build_time}
</div>
)}
</div>
</SidebarFooter>
<SidebarRail /> <SidebarRail />
</Sidebar> </Sidebar>
) )

View file

@ -79,6 +79,12 @@
"labels": { "labels": {
"loading": "Loading..." "loading": "Loading..."
}, },
"footer": {
"version": "Version",
"commit": "Commit",
"build": "Build",
"version_unknown": "Unknown"
},
"credentials": { "credentials": {
"description": "Manage OAuth and token-based credentials for supported providers.", "description": "Manage OAuth and token-based credentials for supported providers.",
"loading": "Loading credentials...", "loading": "Loading credentials...",

View file

@ -79,6 +79,12 @@
"labels": { "labels": {
"loading": "加载中..." "loading": "加载中..."
}, },
"footer": {
"version": "版本",
"commit": "提交",
"build": "构建",
"version_unknown": "未知"
},
"credentials": { "credentials": {
"description": "管理已支持服务商的 OAuth 与 Token 凭据。", "description": "管理已支持服务商的 OAuth 与 Token 凭据。",
"loading": "正在加载凭据...", "loading": "正在加载凭据...",