feat(version): 第18条 显示版本与内部版本解耦(V/A 双系)

- config 新增 displayVersion 注入变量(默认 1.0.0):GetDisplayVersion/
  GetMasterDisplayVersion(V)/GetAgentDisplayVersion(A),以后升版只改注入值
- ParseDisplayVersion/CompareDisplayVersion:三段数值比较,个位进位规则
  (1.0.9<1.1.0) 天然成立;不可解析(dev 标签/3.7.0-agent.1)返回 ok=false
- 显示位切换:dist 注入 X_UI_CUR_VER、-v、TG 报告 → V 系;
  agent 遥测 agentVersion → A 系;节点行 A 系原样显示、老 agent 沿用
  "agent " 前缀兼容
- 内部流不动:GetBaseVersion/GetPanelVersion 及其比较/订阅/远端状态
  消费方保持原样,传统节点更新判定与徽标行为不变
This commit is contained in:
Muse
2026-10-22 13:01:50 +00:00
parent e5f39ed20b
commit 1edb8f71da
7 changed files with 186 additions and 5 deletions
+3 -1
View File
@@ -35,7 +35,9 @@ type telemetryPayload struct {
// collectTelemetry samples every source independently; a failing source // collectTelemetry samples every source independently; a failing source
// only blanks its own field, never the whole report. // only blanks its own field, never the whole report.
func (a *Agent) collectTelemetry() *telemetryPayload { func (a *Agent) collectTelemetry() *telemetryPayload {
t := &telemetryPayload{AgentVersion: config.GetBaseVersion()} // Agents report the A-series display version; the master shows it
// verbatim on agent node rows.
t := &telemetryPayload{AgentVersion: config.GetAgentDisplayVersion()}
if v, ok := a.cachedXrayVersion(); ok { if v, ok := a.cachedXrayVersion(); ok {
t.XrayVersion = v t.XrayVersion = v
} }
+97
View File
@@ -35,6 +35,13 @@ var (
buildCommit string buildCommit string
buildDate string buildDate string
buildVersion string buildVersion string
// displayVersion is this fork's own user-facing release number ("1.0.0"
// in "V1.0.0" / "A1.0.0"), injected via `-ldflags -X` by release builds.
// It is deliberately separate from buildVersion (the upstream-shaped
// internal version): display surfaces (panel header, node list, agent
// telemetry) read the V/A display version, while internal comparisons
// and advertisements keep using GetBaseVersion/GetPanelVersion.
displayVersion string
) )
// LogLevel represents the logging level for the application. // LogLevel represents the logging level for the application.
@@ -103,6 +110,96 @@ func GetPanelVersion() string {
return "dev+" + commit return "dev+" + commit
} }
// defaultDisplayVersion is the display version reported when a build does
// not inject one (plain `go build` / `go run`).
const defaultDisplayVersion = "1.0.0"
// GetDisplayVersion returns the bare numeric display version ("1.0.0"):
// the injected display version when the build provided one, else the
// default. Display versions advance in single-digit carry steps
// (1.0.9 -> 1.1.0 -> ... -> 1.9.9 -> 2.0.0); each segment stays 0-9.
func GetDisplayVersion() string {
if v := strings.TrimSpace(displayVersion); v != "" {
return v
}
return defaultDisplayVersion
}
// GetMasterDisplayVersion is the master's display version ("V1.0.0"),
// shown in the panel's version slot.
func GetMasterDisplayVersion() string {
return "V" + GetDisplayVersion()
}
// GetAgentDisplayVersion is the agent's display version ("A1.0.0"),
// reported in agent telemetry and shown on agent node rows.
func GetAgentDisplayVersion() string {
return "A" + GetDisplayVersion()
}
// ParseDisplayVersion parses a display version into its three numeric
// segments. It accepts the bare form ("1.0.0") and the letter-prefixed
// display forms ("V1.0.0", "A1.0.0", "v1.0.0"). Anything else — dev labels,
// suffixed builds like "3.7.0-agent.1", wrong segment counts, non-numeric
// segments — reports ok=false so callers can fall back instead of guessing.
func ParseDisplayVersion(s string) (parts [3]int, ok bool) {
s = strings.TrimSpace(s)
if parsed, good := parseDisplayParts(s); good {
return parsed, true
}
// Retry once without a single leading display letter (V/A, any case).
if len(s) > 1 && (s[0] == 'V' || s[0] == 'v' || s[0] == 'A' || s[0] == 'a') {
if parsed, good := parseDisplayParts(s[1:]); good {
return parsed, true
}
}
return [3]int{}, false
}
func parseDisplayParts(s string) ([3]int, bool) {
var parts [3]int
segs := strings.Split(s, ".")
if len(segs) != 3 {
return parts, false
}
for i, seg := range segs {
if seg == "" {
return parts, false
}
for _, r := range seg {
if r < '0' || r > '9' {
return parts, false
}
}
n, err := strconv.Atoi(seg)
if err != nil {
return parts, false
}
parts[i] = n
}
return parts, true
}
// CompareDisplayVersion compares two display versions segment by segment
// (numeric, so the single-digit carry rule 1.0.9 < 1.1.0 holds naturally).
// ok=false when either side is not a parseable display version.
func CompareDisplayVersion(a, b string) (cmp int, ok bool) {
pa, okA := ParseDisplayVersion(a)
pb, okB := ParseDisplayVersion(b)
if !okA || !okB {
return 0, false
}
for i := range 3 {
if pa[i] != pb[i] {
if pa[i] < pb[i] {
return -1, true
}
return 1, true
}
}
return 0, true
}
// GetLogLevel returns the current logging level based on environment variables or defaults to Info. // GetLogLevel returns the current logging level based on environment variables or defaults to Info.
func GetLogLevel() LogLevel { func GetLogLevel() LogLevel {
if IsDebug() { if IsDebug() {
+72
View File
@@ -53,6 +53,78 @@ func TestGetBaseVersionBuildVersionOverride(t *testing.T) {
} }
} }
func TestDisplayVersionDefaultsAndOverride(t *testing.T) {
orig := displayVersion
t.Cleanup(func() { displayVersion = orig })
displayVersion = ""
if got := GetDisplayVersion(); got != "1.0.0" {
t.Fatalf("default display version = %q, want 1.0.0", got)
}
if got := GetMasterDisplayVersion(); got != "V1.0.0" {
t.Fatalf("default master display version = %q, want V1.0.0", got)
}
if got := GetAgentDisplayVersion(); got != "A1.0.0" {
t.Fatalf("default agent display version = %q, want A1.0.0", got)
}
displayVersion = "1.0.1"
if got := GetMasterDisplayVersion(); got != "V1.0.1" {
t.Fatalf("injected master display version = %q, want V1.0.1", got)
}
if got := GetAgentDisplayVersion(); got != "A1.0.1" {
t.Fatalf("injected agent display version = %q, want A1.0.1", got)
}
}
func TestParseDisplayVersion(t *testing.T) {
valid := map[string][3]int{
"1.0.0": {1, 0, 0},
"V1.0.0": {1, 0, 0},
"A1.0.9": {1, 0, 9},
"v2.0.0": {2, 0, 0},
" 1.2.3 ": {1, 2, 3},
}
for in, want := range valid {
got, ok := ParseDisplayVersion(in)
if !ok || got != want {
t.Fatalf("ParseDisplayVersion(%q) = %v, %v; want %v, true", in, got, ok, want)
}
}
invalid := []string{"", "3.7.0-agent.1", "dev+1a2b3c4d", "1.0", "1.0.0.1", "1.x.0", "V", "1..0"}
for _, in := range invalid {
if _, ok := ParseDisplayVersion(in); ok {
t.Fatalf("ParseDisplayVersion(%q) ok=true, want false", in)
}
}
}
func TestCompareDisplayVersion(t *testing.T) {
cases := []struct {
a, b string
want int
}{
{"1.0.0", "1.0.0", 0},
{"V1.0.0", "A1.0.0", 0}, // prefix letters are flavour, not value
{"1.0.9", "1.1.0", -1}, // single-digit carry step
{"1.1.0", "1.0.9", 1}, // carry compares numerically, not lexically
{"1.9.9", "2.0.0", -1}, // full carry
{"A1.0.1", "A1.0.0", 1}, // agent series
{"V1.0.1", "1.0.0", 1}, // release tag vs current
{"2.0.0", "1.9.9", 1},
{"1.0.0", "1.0.1", -1},
}
for _, tc := range cases {
got, ok := CompareDisplayVersion(tc.a, tc.b)
if !ok || got != tc.want {
t.Fatalf("CompareDisplayVersion(%q, %q) = %d, %v; want %d, true", tc.a, tc.b, got, ok, tc.want)
}
}
if _, ok := CompareDisplayVersion("3.7.0-agent.1", "1.0.0"); ok {
t.Fatal("unparseable side must report ok=false")
}
}
func TestGetPortOverride(t *testing.T) { func TestGetPortOverride(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
+3 -1
View File
@@ -86,7 +86,9 @@ func serveDistPage(c *gin.Context, name string) {
} }
script := `<script data-cfasync="false"` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"` script := `<script data-cfasync="false"` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
if name != "login.html" { if name != "login.html" {
escapedVer := jsEscape.Replace(config.GetPanelVersion()) // The version slot shows the fork's display version (V-series);
// internal version comparisons keep using GetPanelVersion.
escapedVer := jsEscape.Replace(config.GetMasterDisplayVersion())
script += `;window.X_UI_CUR_VER="` + escapedVer + `"` script += `;window.X_UI_CUR_VER="` + escapedVer + `"`
script += `;window.X_UI_DB_TYPE="` + config.GetDBKind() + `"` script += `;window.X_UI_DB_TYPE="` + config.GetDBKind() + `"`
} }
+9 -1
View File
@@ -3,6 +3,7 @@ package service
import ( import (
"strings" "strings"
"github.com/mhsanaei/3x-ui/v3/internal/config"
"github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/common" "github.com/mhsanaei/3x-ui/v3/internal/util/common"
@@ -199,7 +200,14 @@ func annotateAgentTelemetry(views []*NodeView) {
v.XrayVersion = ag.XrayVersion v.XrayVersion = ag.XrayVersion
} }
if ag.AgentVersion != "" { if ag.AgentVersion != "" {
v.PanelVersion = "agent " + ag.AgentVersion // New agents report the A-series display version, shown
// verbatim; older agents report the internal build version
// and keep the historical "agent " prefix.
if _, ok := config.ParseDisplayVersion(ag.AgentVersion); ok && strings.HasPrefix(ag.AgentVersion, "A") {
v.PanelVersion = ag.AgentVersion
} else {
v.PanelVersion = "agent " + ag.AgentVersion
}
} }
v.UptimeSecs = ag.UptimeSecs v.UptimeSecs = ag.UptimeSecs
v.LatencyMs = ag.LatencyMs v.LatencyMs = ag.LatencyMs
+1 -1
View File
@@ -112,7 +112,7 @@ func (t *Tgbot) prepareServerUsageInfo() string {
onlines := t.inboundService.GetOnlineClients() onlines := t.inboundService.GetOnlineClients()
info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetPanelVersion()) info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetMasterDisplayVersion())
info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version)) info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version))
// get ip address // get ip address
+1 -1
View File
@@ -592,7 +592,7 @@ func main() {
flag.Parse() flag.Parse()
if showVersion { if showVersion {
fmt.Println(config.GetPanelVersion()) fmt.Println(config.GetMasterDisplayVersion())
return return
} }