Procházet zdrojové kódy

feat(panel): surface dev-build version in UI, bot, and CLI

A dev build now shows its `dev+<commit>` identity instead of a misleading stable-looking version in the sidebar badge, dashboard card, update modal, Telegram status report, startup log, and `x-ui -v`. Adds a shared formatPanelVersion helper (single v prefix; dev labels shown verbatim) and fixes the mobile-tag double-v.

Renames the version getters for clarity: config.GetVersion to GetBaseVersion (raw embedded version), config.GetReportedVersion to GetPanelVersion (advertised/displayed), and the xray process GetVersion to GetXrayVersion.
MHSanaei před 16 hodinami
rodič
revize
e4b881e58a

+ 2 - 1
frontend/src/layouts/AppSidebar.tsx

@@ -33,6 +33,7 @@ import {
 } from '@ant-design/icons';
 
 import { HttpUtil } from '@/utils';
+import { formatPanelVersion } from '@/lib/panel-version';
 import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme';
 import { useAllSettings } from '@/api/queries/useAllSettings';
 import './AppSidebar.css';
@@ -84,7 +85,7 @@ function DonateButton({ ariaLabel }: { ariaLabel: string }) {
 
 function VersionBadge({ version, collapsed }: { version: string; collapsed?: boolean }) {
   if (!version) return null;
-  const label = `v${version}`;
+  const label = formatPanelVersion(version);
   return (
     <a
       href={REPO_URL}

+ 12 - 0
frontend/src/lib/panel-version.ts

@@ -14,6 +14,18 @@ function parseVersionParts(version: string): [number, number, number] | null {
   return [out[0], out[1], out[2]];
 }
 
+// Format a panel version for display. Dev builds report a "dev+<commit>"
+// identity (see config.GetPanelVersion); show those — and any other
+// non-numeric label — verbatim. Semantic versions get a single normalized "v"
+// prefix, so a raw "v3.4.0" tag and a bare "3.4.0" both render as "v3.4.0"
+// instead of doubling up to "vv3.4.0".
+export function formatPanelVersion(version: string | undefined | null): string {
+  const v = (version || '').trim();
+  if (!v) return '';
+  const normalized = v.replace(/^v/i, '');
+  return /^\d/.test(normalized) ? `v${normalized}` : v;
+}
+
 export function isPanelUpdateAvailable(latest: string, current: string): boolean {
   if (!latest || !current) return false;
   const a = parseVersionParts(latest);

+ 6 - 7
frontend/src/pages/index/IndexPage.tsx

@@ -37,6 +37,7 @@ import {
 } from '@ant-design/icons';
 
 import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
+import { formatPanelVersion } from '@/lib/panel-version';
 import { useTheme } from '@/hooks/useTheme';
 import { useStatusQuery } from '@/api/queries/useStatusQuery';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
@@ -104,7 +105,7 @@ export default function IndexPage() {
   }, []);
 
   const displayVersion = useMemo(
-    () => panelUpdateInfo.currentVersion || window.X_UI_CUR_VER || '?',
+    () => window.X_UI_CUR_VER || panelUpdateInfo.currentVersion || '?',
     [panelUpdateInfo.currentVersion],
   );
 
@@ -240,10 +241,8 @@ export default function IndexPage() {
                           {isMobile && displayVersion && (
                             <Tag color={panelUpdateInfo.updateAvailable ? 'orange' : 'green'}>
                               {panelUpdateInfo.updateAvailable
-                                ? panelUpdateInfo.channel === 'dev'
-                                  ? panelUpdateInfo.latestVersion
-                                  : `v${panelUpdateInfo.latestVersion}`
-                                : `v${displayVersion}`}
+                                ? formatPanelVersion(panelUpdateInfo.latestVersion)
+                                : formatPanelVersion(displayVersion)}
                             </Tag>
                           )}
                         </Space>
@@ -272,8 +271,8 @@ export default function IndexPage() {
                           {!isMobile && (
                             <span>
                               {panelUpdateInfo.updateAvailable
-                                ? `${t('update')} ${panelUpdateInfo.latestVersion}`
-                                : `v${displayVersion}`}
+                                ? `${t('update')} ${formatPanelVersion(panelUpdateInfo.latestVersion)}`
+                                : formatPanelVersion(displayVersion)}
                             </span>
                           )}
                         </Space>,

+ 2 - 1
frontend/src/pages/index/PanelUpdateModal.tsx

@@ -5,6 +5,7 @@ import { CloudDownloadOutlined } from '@ant-design/icons';
 import axios from 'axios';
 
 import { HttpUtil, PromiseUtil } from '@/utils';
+import { formatPanelVersion } from '@/lib/panel-version';
 import './PanelUpdateModal.css';
 
 export interface PanelUpdateInfo {
@@ -140,7 +141,7 @@ export default function PanelUpdateModal({
             {isDev ? (
               <Tag color="green">{info.currentCommit || '?'}</Tag>
             ) : (
-              <Tag color="green">v{info.currentVersion || '?'}</Tag>
+              <Tag color="green">{formatPanelVersion(window.X_UI_CUR_VER || info.currentVersion) || '?'}</Tag>
             )}
           </div>
           {info.updateAvailable ? (

+ 24 - 1
frontend/src/test/panel-version.test.ts

@@ -1,6 +1,6 @@
 import { describe, it, expect } from 'vitest';
 
-import { isPanelUpdateAvailable } from '@/lib/panel-version';
+import { formatPanelVersion, isPanelUpdateAvailable } from '@/lib/panel-version';
 
 // Parity with web/service/panel.go isNewerVersion.
 describe('isPanelUpdateAvailable', () => {
@@ -31,3 +31,26 @@ describe('isPanelUpdateAvailable', () => {
     expect(isPanelUpdateAvailable('nightly-1', 'nightly-1')).toBe(false);
   });
 });
+
+describe('formatPanelVersion', () => {
+  it('adds a single v prefix to bare semantic versions', () => {
+    expect(formatPanelVersion('3.4.0')).toBe('v3.4.0');
+    expect(formatPanelVersion('2.6.5')).toBe('v2.6.5');
+  });
+
+  it('does not double up the v on already-prefixed tags', () => {
+    expect(formatPanelVersion('v3.4.0')).toBe('v3.4.0');
+    expect(formatPanelVersion('V3.4.0')).toBe('v3.4.0');
+  });
+
+  it('shows dev builds verbatim without a v prefix', () => {
+    expect(formatPanelVersion('dev+1a2b3c4d')).toBe('dev+1a2b3c4d');
+    expect(formatPanelVersion('dev')).toBe('dev');
+  });
+
+  it('returns empty for blank input and leaves unknown markers untouched', () => {
+    expect(formatPanelVersion('')).toBe('');
+    expect(formatPanelVersion(undefined)).toBe('');
+    expect(formatPanelVersion('?')).toBe('?');
+  });
+});

+ 12 - 9
internal/config/config.go

@@ -41,8 +41,11 @@ const (
 	Error   LogLevel = "error"
 )
 
-// GetVersion returns the version string of the 3x-ui application.
-func GetVersion() string {
+// GetBaseVersion returns the raw embedded release version of the 3x-ui panel
+// (e.g. "3.4.0"). This is the panel's own version, not the Xray version. For the
+// version a panel advertises/displays (which adds a "dev+<sha>" label on dev
+// builds), use GetPanelVersion.
+func GetBaseVersion() string {
 	return strings.TrimSpace(version)
 }
 
@@ -68,14 +71,14 @@ func IsDevBuild() bool {
 	return GetBuildCommit() != ""
 }
 
-// GetReportedVersion returns the version a panel advertises to a managing master
-// node: the plain version for stable builds, or "dev+<short commit>" for dev
-// builds. The dev form mirrors the master's getPanelUpdateInfo latestVersion so
-// a node on the current dev commit compares as up to date instead of always
-// showing "update available".
-func GetReportedVersion() string {
+// GetPanelVersion returns the version a panel advertises to a managing master
+// node and displays in the UI: the plain version for stable builds, or
+// "dev+<short commit>" for dev builds. The dev form mirrors the master's
+// getPanelUpdateInfo latestVersion so a node on the current dev commit compares
+// as up to date instead of always showing "update available".
+func GetPanelVersion() string {
 	if !IsDevBuild() {
-		return GetVersion()
+		return GetBaseVersion()
 	}
 	commit := GetBuildCommit()
 	if len(commit) > 8 {

+ 7 - 7
internal/config/config_test.go

@@ -5,23 +5,23 @@ import (
 	"testing"
 )
 
-func TestGetReportedVersion(t *testing.T) {
+func TestGetPanelVersion(t *testing.T) {
 	orig := buildCommit
 	t.Cleanup(func() { buildCommit = orig })
 
 	buildCommit = ""
-	if got := GetReportedVersion(); got != GetVersion() {
-		t.Fatalf("stable build: GetReportedVersion = %q, want %q", got, GetVersion())
+	if got := GetPanelVersion(); got != GetBaseVersion() {
+		t.Fatalf("stable build: GetPanelVersion = %q, want %q", got, GetBaseVersion())
 	}
 
 	buildCommit = "1d1128cf"
-	if got := GetReportedVersion(); got != "dev+1d1128cf" {
-		t.Fatalf("dev build: GetReportedVersion = %q, want %q", got, "dev+1d1128cf")
+	if got := GetPanelVersion(); got != "dev+1d1128cf" {
+		t.Fatalf("dev build: GetPanelVersion = %q, want %q", got, "dev+1d1128cf")
 	}
 
 	buildCommit = "1d1128cf945c4615efa05cf41ba7fa766e2ee428"
-	if got := GetReportedVersion(); got != "dev+1d1128cf" {
-		t.Fatalf("dev build (full sha): GetReportedVersion = %q, want %q", got, "dev+1d1128cf")
+	if got := GetPanelVersion(); got != "dev+1d1128cf" {
+		t.Fatalf("dev build (full sha): GetPanelVersion = %q, want %q", got, "dev+1d1128cf")
 	}
 }
 

+ 1 - 1
internal/web/controller/dist.go

@@ -112,7 +112,7 @@ func serveDistPage(c *gin.Context, name string) {
 	}
 	script := `<script` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
 	if name != "login.html" {
-		escapedVer := jsEscape.Replace(config.GetVersion())
+		escapedVer := jsEscape.Replace(config.GetPanelVersion())
 		script += `;window.X_UI_CUR_VER="` + escapedVer + `"`
 		script += `;window.X_UI_DB_TYPE="` + config.GetDBKind() + `"`
 	}

+ 2 - 2
internal/web/service/panel/panel.go

@@ -80,7 +80,7 @@ func (s *PanelService) GetUpdateInfo() (*PanelUpdateInfo, error) {
 	if err != nil {
 		return nil, err
 	}
-	current := config.GetVersion()
+	current := config.GetBaseVersion()
 	return &PanelUpdateInfo{
 		Channel:         "stable",
 		CurrentVersion:  current,
@@ -114,7 +114,7 @@ func getDevUpdateInfo() (*PanelUpdateInfo, error) {
 	currentCommit := config.GetBuildCommit()
 	return &PanelUpdateInfo{
 		Channel:         "dev",
-		CurrentVersion:  config.GetVersion(),
+		CurrentVersion:  config.GetPanelVersion(),
 		CurrentCommit:   shortCommit(currentCommit),
 		LatestCommit:    shortCommit(latestCommit),
 		LatestVersion:   "dev+" + shortCommit(latestCommit),

+ 1 - 1
internal/web/service/server.go

@@ -604,7 +604,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
 		status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
 	}
 	status.Xray.Version = s.xrayService.GetXrayVersion()
-	status.PanelVersion = config.GetReportedVersion()
+	status.PanelVersion = config.GetPanelVersion()
 	if guid, err := s.settingService.GetPanelGuid(); err == nil {
 		status.PanelGuid = guid
 	}

+ 1 - 1
internal/web/service/tgbot/tgbot_report.go

@@ -108,7 +108,7 @@ func (t *Tgbot) prepareServerUsageInfo() string {
 	onlines := service.XrayProcess().GetOnlineClients()
 
 	info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
-	info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion())
+	info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetPanelVersion())
 	info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version))
 
 	// get ip address

+ 1 - 1
internal/web/service/xray.go

@@ -96,7 +96,7 @@ func (s *XrayService) GetXrayVersion() string {
 	if p == nil {
 		return "Unknown"
 	}
-	return p.GetVersion()
+	return p.GetXrayVersion()
 }
 
 // RemoveIndex removes an element at the specified index from a slice.

+ 2 - 2
internal/xray/process.go

@@ -270,8 +270,8 @@ func (p *process) GetResult() string {
 	return lastLine
 }
 
-// GetVersion returns the version string of the Xray process.
-func (p *process) GetVersion() string {
+// GetXrayVersion returns the version string of the Xray process.
+func (p *process) GetXrayVersion() string {
 	return p.version
 }
 

+ 2 - 2
main.go

@@ -34,7 +34,7 @@ import (
 
 // runWebServer initializes and starts the web server for the 3x-ui panel.
 func runWebServer() {
-	log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
+	log.Printf("Starting %v %v", config.GetName(), config.GetPanelVersion())
 
 	switch config.GetLogLevel() {
 	case config.Debug:
@@ -587,7 +587,7 @@ func main() {
 
 	flag.Parse()
 	if showVersion {
-		fmt.Println(config.GetVersion())
+		fmt.Println(config.GetPanelVersion())
 		return
 	}