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:
@@ -35,7 +35,9 @@ type telemetryPayload struct {
|
||||
// collectTelemetry samples every source independently; a failing source
|
||||
// only blanks its own field, never the whole report.
|
||||
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 {
|
||||
t.XrayVersion = v
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@ var (
|
||||
buildCommit string
|
||||
buildDate 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.
|
||||
@@ -103,6 +110,96 @@ func GetPanelVersion() string {
|
||||
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.
|
||||
func GetLogLevel() LogLevel {
|
||||
if IsDebug() {
|
||||
|
||||
@@ -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) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -86,7 +86,9 @@ func serveDistPage(c *gin.Context, name string) {
|
||||
}
|
||||
script := `<script data-cfasync="false"` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
|
||||
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_DB_TYPE="` + config.GetDBKind() + `"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"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/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
||||
@@ -199,7 +200,14 @@ func annotateAgentTelemetry(views []*NodeView) {
|
||||
v.XrayVersion = ag.XrayVersion
|
||||
}
|
||||
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.LatencyMs = ag.LatencyMs
|
||||
|
||||
@@ -112,7 +112,7 @@ func (t *Tgbot) prepareServerUsageInfo() string {
|
||||
onlines := t.inboundService.GetOnlineClients()
|
||||
|
||||
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))
|
||||
|
||||
// get ip address
|
||||
|
||||
Reference in New Issue
Block a user