fix(version): 界面版本号改为动态读取,统一版本为 v1.0.2

界面侧栏与页脚硬编码 v1.0.0,发版后始终显示陈旧版本。

- 新增 useAppVersion hook:从后端 /version 读取并做模块级缓存
- Sidebar / MainLayout 改用该 hook
- /agent/arch 的 version 字段原先硬编码 "1.0.0",改由 main 注入 router.AppVersion
- Dockerfile 增加 AGENT_VERSION 构建参数,与镜像 VERSION 解耦,
  避免纯前端改动也触发全量 Agent 重推

统一版本:镜像 lghost/aiops:v1.0.2,内置 Agent v1.0.2。

// Logan.Li
This commit is contained in:
Logan.Li committed 2026-09-14 08:46:40 +00:00
1 parent 9360ebefd8
commit 7a96381dc7
7 files changed
+79 -6

No files matched your search

+4 -3
View File
@@ -75,6 +75,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
FROM golang:1.24-alpine AS agent-builder FROM golang:1.24-alpine AS agent-builder
ARG VERSION ARG VERSION
ARG AGENT_VERSION
ARG BUILD_TIME ARG BUILD_TIME
ARG TARGETARCH ARG TARGETARCH
@@ -92,13 +93,13 @@ COPY agent/ ./
# Go 在 CGO_ENABLED=0 下可原生交叉编译,无需 QEMU,故一次性产出全部目标, # Go 在 CGO_ENABLED=0 下可原生交叉编译,无需 QEMU,故一次性产出全部目标,
# 与 router/agent_static.go 的下载清单保持一致,避免 ARM 主机下载 404。 // Logan.Li # 与 router/agent_static.go 的下载清单保持一致,避免 ARM 主机下载 404。 // Logan.Li
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" \ -ldflags="-s -w -X main.version=${AGENT_VERSION} -X main.buildTime=${BUILD_TIME}" \
-o bin/aiops-agent-linux-amd64 . -o bin/aiops-agent-linux-amd64 .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \ RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
-ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" \ -ldflags="-s -w -X main.version=${AGENT_VERSION} -X main.buildTime=${BUILD_TIME}" \
-o bin/aiops-agent-linux-arm64 . -o bin/aiops-agent-linux-arm64 .
RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \ RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \
-ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" \ -ldflags="-s -w -X main.version=${AGENT_VERSION} -X main.buildTime=${BUILD_TIME}" \
-o bin/aiops-agent-windows-amd64.exe . -o bin/aiops-agent-windows-amd64.exe .
# ==================== Stage 4: Runtime ==================== # ==================== Stage 4: Runtime ====================
+2
View File
@@ -215,6 +215,8 @@ func main() {
systask.Init(db) systask.Init(db)
// 注册路由 // 注册路由
// 把编译期注入的版本号透传给 router,供 /agent/arch 返回真实版本。 // Logan.Li
router.AppVersion = Version
router.Register(r, db, log, cfg, cache) router.Register(r, db, log, cfg, cache)
// 用户定时任务调度(Linux 5 段 crontab)// Logan.Li // 用户定时任务调度(Linux 5 段 crontab)// Logan.Li
+5 -1
View File
@@ -22,6 +22,10 @@ var agentBinaryFiles = map[string]string{
"windows-amd64": "aiops-agent-windows-amd64.exe", "windows-amd64": "aiops-agent-windows-amd64.exe",
} }
// AppVersion 由 main 在启动时注入,用于 /agent/arch 返回真实版本号;
// 此前该字段硬编码为 "1.0.0",发版后接口版本与实际不符。 // Logan.Li
var AppVersion = "dev"
// registerAgentDownloads 注册 Agent 安装脚本和二进制下载路由,避免维护 backend/agent 重复目录。 // Logan.Li // registerAgentDownloads 注册 Agent 安装脚本和二进制下载路由,避免维护 backend/agent 重复目录。 // Logan.Li
func registerAgentDownloads(r *gin.Engine, log *zap.Logger) { func registerAgentDownloads(r *gin.Engine, log *zap.Logger) {
paths := resolveAgentStaticPaths(log) paths := resolveAgentStaticPaths(log)
@@ -34,7 +38,7 @@ func registerAgentDownloads(r *gin.Engine, log *zap.Logger) {
} }
downloads := availableAgentDownloads(paths.binDir) downloads := availableAgentDownloads(paths.binDir)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"version": "1.0.0", "version": AppVersion,
"downloads": downloads, "downloads": downloads,
"install": fmt.Sprintf("curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token <token>", base, base), "install": fmt.Sprintf("curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token <token>", base, base),
}) })
+2
View File
@@ -61,6 +61,8 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
VERSION: ${VERSION:-dev} VERSION: ${VERSION:-dev}
# AGENT_VERSION 与镜像版本解耦:纯后端/前端改动不必让所有主机重推 Agent。 // Logan.Li
AGENT_VERSION: ${AGENT_VERSION:-v1.0.2}
BUILD_TIME: ${BUILD_TIME:-unknown} BUILD_TIME: ${BUILD_TIME:-unknown}
GIT_COMMIT: ${GIT_COMMIT:-unknown} GIT_COMMIT: ${GIT_COMMIT:-unknown}
container_name: aiops container_name: aiops
@@ -9,6 +9,7 @@ import { authApi } from '@/api';
import type { JsonData } from '@/types/common'; import type { JsonData } from '@/types/common';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SessionWatermark } from '@/components/common/SessionWatermark'; import { SessionWatermark } from '@/components/common/SessionWatermark';
import { useAppVersion } from '@/hooks/use-app-version';
import { shouldProtectPage } from '@/components/common/sensitivePageWatermark'; import { shouldProtectPage } from '@/components/common/sensitivePageWatermark';
/** /**
@@ -16,6 +17,8 @@ import { shouldProtectPage } from '@/components/common/sensitivePageWatermark';
* 侧栏 + 顶栏 + 面包屑 + 内容区,间距由 --layout-* 令牌驱动 // Logan.Li * 侧栏 + 顶栏 + 面包屑 + 内容区,间距由 --layout-* 令牌驱动 // Logan.Li
*/ */
export function MainLayout() { export function MainLayout() {
// 页脚版本号同样取自后端 /version。 // Logan.Li
const appVersion = useAppVersion();
const { t } = useTranslation(); const { t } = useTranslation();
const { sidebarCollapsed } = useThemeStore(); const { sidebarCollapsed } = useThemeStore();
const token = useUserStore((s) => s.token); const token = useUserStore((s) => s.token);
@@ -97,7 +100,7 @@ export function MainLayout() {
<footer className="dg-app-footer"> <footer className="dg-app-footer">
<div className="flex items-center justify-between text-xs dg-text-muted"> <div className="flex items-center justify-between text-xs dg-text-muted">
<span>{t('login.title')}</span> <span>{t('login.title')}</span>
<span>v1.0.0</span> <span>{appVersion || 'v1.0.0'}</span>
</div> </div>
</footer> </footer>
</div> </div>
+4 -1
View File
@@ -44,6 +44,7 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useAppVersion } from '@/hooks/use-app-version';
interface MenuItem { interface MenuItem {
id: string; id: string;
@@ -363,6 +364,8 @@ interface SidebarProps {
} }
export function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) { export function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
// 版本号从后端 /version 读取,避免发版后界面仍显示硬编码的旧版本。 // Logan.Li
const appVersion = useAppVersion();
const { t } = useTranslation(); const { t } = useTranslation();
const { sidebarCollapsed, toggleSidebar } = useThemeStore(); const { sidebarCollapsed, toggleSidebar } = useThemeStore();
const { hasPermission } = usePermission(); const { hasPermission } = usePermission();
@@ -433,7 +436,7 @@ export function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
{!isCollapsed && ( {!isCollapsed && (
<div className="px-3 py-2 border-t border-[var(--dg-border)] flex-shrink-0"> <div className="px-3 py-2 border-t border-[var(--dg-border)] flex-shrink-0">
<div className="text-[10px] text-[var(--dg-text-muted)] text-center"> <div className="text-[10px] text-[var(--dg-text-muted)] text-center">
AIOps v1.0.0 AIOps {appVersion || 'v1.0.0'}
</div> </div>
</div> </div>
)} )}
+58
View File
@@ -0,0 +1,58 @@
import { useEffect, useState } from 'react';
/**
* 应用版本信息。
*
* 版本号此前在后端 /version 已提供(含 build_time / git_commit),
* 但侧边栏与页脚硬编码了 v1.0.0,导致发版后界面显示陈旧。
* 这里统一从 /version 读取并做模块级缓存,避免多处重复请求。 // Logan.Li
*/
export interface AppVersion {
version: string;
build_time?: string;
git_commit?: string;
mode?: string;
}
// 模块级缓存:整个会话只请求一次。
let cached: AppVersion | null = null;
let inflight: Promise<AppVersion | null> | null = null;
function fetchVersion(): Promise<AppVersion | null> {
if (cached) return Promise.resolve(cached);
if (inflight) return inflight;
// 注意:/version 挂在根路径,不带 /api/v1 前缀,故不能用统一 request 封装。
inflight = fetch('/version')
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data && typeof data.version === 'string') {
cached = data as AppVersion;
return cached;
}
return null;
})
.catch(() => null)
.finally(() => {
inflight = null;
});
return inflight;
}
/** 读取后端版本号;失败时返回空串,调用方负责兜底显示。 */
export function useAppVersion(): string {
const [version, setVersion] = useState<string>(cached?.version ?? '');
useEffect(() => {
let alive = true;
fetchVersion().then((v) => {
if (alive && v?.version) setVersion(v.version);
});
return () => {
alive = false;
};
}, []);
return version;
}