Przeglądaj źródła

fix(sub): {{INBOUND}} = inbound remark, fix {{TRAFFIC_LEFT}} across inbounds (#5443)

Issue 1: the host endpoint remark no longer substitutes the inbound remark
as the config name. {{INBOUND}} always resolves to the inbound's own remark
and {{HOST}} to the host remark, so both can be shown side by side instead
of the host name appearing twice. configName() drops hostRemark entirely;
token help text updated in all locales.

Issue 2: client_traffics.email is globally unique, so a client shared across
several inbounds of one subscription has a single traffic row owned by one
inbound. statsForClient only searched the current inbound's preloaded
ClientStats, missing on every other inbound's link and falling back to
Up=Down=0 -- so {{TRAFFIC_LEFT}} printed the full quota. Build a per-request
email->stats map from all the subscription's inbounds (no extra queries) and
fall back to it.
MHSanaei 1 dzień temu
rodzic
commit
6d9fd4b41b

+ 2 - 1
internal/sub/controller.go

@@ -149,7 +149,8 @@ func (a *SUBController) subs(c *gin.Context) {
 	} else {
 	} else {
 		var result strings.Builder
 		var result strings.Builder
 		for _, sub := range subs {
 		for _, sub := range subs {
-			result.WriteString(sub + "\n")
+			result.WriteString(sub)
+			result.WriteString("\n")
 		}
 		}
 
 
 		// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
 		// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here

+ 18 - 12
internal/sub/remark_vars.go

@@ -15,8 +15,9 @@ import (
 // remarkContext carries the per-client data a remark template can interpolate.
 // remarkContext carries the per-client data a remark template can interpolate.
 // stats holds the live traffic record when one exists; when it doesn't, the
 // stats holds the live traffic record when one exists; when it doesn't, the
 // caller synthesizes a minimal one from the client so expiry/total/status tokens
 // caller synthesizes a minimal one from the client so expiry/total/status tokens
-// still resolve. hostRemark is the host endpoint's own remark: it takes priority
-// over the inbound's remark as the config name and backs the {{HOST}} token.
+// still resolve. hostRemark is the host endpoint's own remark; it backs the
+// {{HOST}} token only — it never substitutes the inbound's remark as the config
+// name (use {{INBOUND}} and {{HOST}} side by side to show both).
 type remarkContext struct {
 type remarkContext struct {
 	client     model.Client
 	client     model.Client
 	stats      xray.ClientTraffic
 	stats      xray.ClientTraffic
@@ -24,12 +25,9 @@ type remarkContext struct {
 	hostRemark string
 	hostRemark string
 }
 }
 
 
-// configName is the display name for a link: the host endpoint's own remark when
-// it has one, otherwise the inbound's remark.
+// configName is the display name for a link: always the inbound's own remark.
+// The host endpoint's remark is surfaced only through the {{HOST}} token.
 func (ctx remarkContext) configName() string {
 func (ctx remarkContext) configName() string {
-	if ctx.hostRemark != "" {
-		return ctx.hostRemark
-	}
 	if ctx.inbound != nil {
 	if ctx.inbound != nil {
 		return ctx.inbound.Remark
 		return ctx.inbound.Remark
 	}
 	}
@@ -227,6 +225,14 @@ func (s *SubService) statsForClient(inbound *model.Inbound, client model.Client)
 	if stats, ok := s.findClientStats(inbound, client.Email); ok {
 	if stats, ok := s.findClientStats(inbound, client.Email); ok {
 		return stats
 		return stats
 	}
 	}
+	// client_traffics.email is globally unique, so a client shared across several
+	// inbounds of one subscription has a single traffic row owned by exactly one
+	// inbound. On every other inbound's link findClientStats misses; fall back to
+	// the per-request map built from all the subscription's inbounds so
+	// {{TRAFFIC_*}} reflect real usage instead of the full quota (#5443).
+	if stats, ok := s.statsByEmail[client.Email]; ok {
+		return stats
+	}
 	return xray.ClientTraffic{
 	return xray.ClientTraffic{
 		Enable:     client.Enable,
 		Enable:     client.Enable,
 		ExpiryTime: client.ExpiryTime,
 		ExpiryTime: client.ExpiryTime,
@@ -292,8 +298,8 @@ func (s *SubService) effectiveTemplate(email string) string {
 }
 }
 
 
 // genTemplatedRemark expands the remark template for one client. hostRemark is
 // genTemplatedRemark expands the remark template for one client. hostRemark is
-// the host endpoint's remark (empty for a plain inbound); it takes priority over
-// the inbound remark for the config name and backs the {{HOST}} token.
+// the host endpoint's remark (empty for a plain inbound); it backs the {{HOST}}
+// token only and never substitutes the inbound remark as the config name.
 func (s *SubService) genTemplatedRemark(inbound *model.Inbound, client model.Client, hostRemark string) string {
 func (s *SubService) genTemplatedRemark(inbound *model.Inbound, client model.Client, hostRemark string) string {
 	ctx := remarkContext{
 	ctx := remarkContext{
 		client:     client,
 		client:     client,
@@ -311,9 +317,9 @@ func (s *SubService) genTemplatedRemark(inbound *model.Inbound, client model.Cli
 }
 }
 
 
 // genHostRemark builds one host endpoint's remark for a specific client. The
 // genHostRemark builds one host endpoint's remark for a specific client. The
-// config name is the host endpoint's own remark when set, otherwise the inbound's
-// remark. In the subscription body the rest of the remark template still applies;
-// displays show just the config name.
+// config name is always the inbound's own remark; the host's remark is surfaced
+// only through the {{HOST}} token. In the subscription body the rest of the
+// remark template still applies; displays show just the config name.
 func (s *SubService) genHostRemark(inbound *model.Inbound, client model.Client, hostRemark string) string {
 func (s *SubService) genHostRemark(inbound *model.Inbound, client model.Client, hostRemark string) string {
 	if !s.subscriptionBody {
 	if !s.subscriptionBody {
 		return remarkContext{inbound: inbound, hostRemark: hostRemark}.configName()
 		return remarkContext{inbound: inbound, hostRemark: hostRemark}.configName()

+ 40 - 20
internal/sub/remark_vars_test.go

@@ -165,30 +165,30 @@ func hostRemarkService(template string) (*SubService, *model.Inbound, model.Clie
 	return s, inbound, client
 	return s, inbound, client
 }
 }
 
 
-// The config name prefers the host endpoint's own remark; the inbound's remark is
-// the fallback, used only when the host has none.
-func TestGenHostRemark_ConfigNameHostWins(t *testing.T) {
+// The config name is always the inbound's own remark; the host endpoint's remark
+// never substitutes it (it is reachable only through {{HOST}}).
+func TestGenHostRemark_ConfigNameUsesInbound(t *testing.T) {
 	s, inbound, client := hostRemarkService("") // no template → config name only
 	s, inbound, client := hostRemarkService("") // no template → config name only
-	if got := s.genHostRemark(inbound, client, "Relay"); got != "Relay" {
-		t.Fatalf("genHostRemark = %q, want %q (host remark wins)", got, "Relay")
+	if got := s.genHostRemark(inbound, client, "Relay"); got != "DE" {
+		t.Fatalf("genHostRemark = %q, want %q (inbound remark, host ignored)", got, "DE")
 	}
 	}
 	if got := s.genHostRemark(inbound, client, ""); got != "DE" {
 	if got := s.genHostRemark(inbound, client, ""); got != "DE" {
-		t.Fatalf("genHostRemark (no host remark) = %q, want %q (inbound fallback)", got, "DE")
+		t.Fatalf("genHostRemark (no host remark) = %q, want %q", got, "DE")
 	}
 	}
 }
 }
 
 
-// In the body the template applies: {{INBOUND}} is the config name (host remark
-// first, inbound fallback) and {{HOST}} is always the host's own remark.
+// In the body the template applies: {{INBOUND}} is always the inbound's remark
+// and {{HOST}} the host's own remark, so the two can be shown side by side.
 func TestGenHostRemark_GlobalTemplate(t *testing.T) {
 func TestGenHostRemark_GlobalTemplate(t *testing.T) {
-	// Host remark set → {{INBOUND}} resolves to it (host wins over the inbound).
+	// {{INBOUND}} resolves to the inbound remark regardless of the host remark.
 	s, inbound, client := hostRemarkService("{{INBOUND}} | {{TRAFFIC_LEFT}} | {{DAYS_LEFT}}d")
 	s, inbound, client := hostRemarkService("{{INBOUND}} | {{TRAFFIC_LEFT}} | {{DAYS_LEFT}}d")
-	if got := s.genHostRemark(inbound, client, "CDN"); got != "CDN | 80.00GB | 10d" {
-		t.Fatalf("global template (host wins) = %q", got)
+	if got := s.genHostRemark(inbound, client, "CDN"); got != "DE | 80.00GB | 10d" {
+		t.Fatalf("global template ({{INBOUND}} = inbound) = %q", got)
 	}
 	}
-	// No host remark → {{INBOUND}} falls back to the inbound's own remark.
-	s2, inbound2, client2 := hostRemarkService("{{INBOUND}} | {{TRAFFIC_LEFT}}")
-	if got := s2.genHostRemark(inbound2, client2, ""); got != "DE | 80.00GB" {
-		t.Fatalf("global template (inbound fallback) = %q", got)
+	// {{INBOUND}} and {{HOST}} side by side show both, distinctly (#5443).
+	s2, inbound2, client2 := hostRemarkService("{{INBOUND}}|{{HOST}}|{{TRAFFIC_LEFT}}")
+	if got := s2.genHostRemark(inbound2, client2, "CDN"); got != "DE|CDN|80.00GB" {
+		t.Fatalf("global template (inbound + host) = %q, want %q", got, "DE|CDN|80.00GB")
 	}
 	}
 	// {{HOST}} is the host's own remark even when the inbound has one of its own.
 	// {{HOST}} is the host's own remark even when the inbound has one of its own.
 	s3, inbound3, client3 := hostRemarkService("{{HOST}}")
 	s3, inbound3, client3 := hostRemarkService("{{HOST}}")
@@ -239,12 +239,12 @@ func TestUsageOnFirstLinkOnly(t *testing.T) {
 func TestRemarkInDisplayContext(t *testing.T) {
 func TestRemarkInDisplayContext(t *testing.T) {
 	s, inbound, client := hostRemarkService("{{INBOUND}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D")
 	s, inbound, client := hostRemarkService("{{INBOUND}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D")
 	s.subscriptionBody = false
 	s.subscriptionBody = false
-	// A host link in a display shows only the config name — host remark wins, with
-	// no per-client email or usage info.
-	if got := s.genHostRemark(inbound, client, "CDN"); got != "CDN" {
-		t.Fatalf("display host link = %q, want config name %q (host wins)", got, "CDN")
+	// A host link in a display shows only the config name — the inbound's remark,
+	// with no per-client email or usage info and the host remark ignored.
+	if got := s.genHostRemark(inbound, client, "CDN"); got != "DE" {
+		t.Fatalf("display host link = %q, want config name %q", got, "DE")
 	}
 	}
-	// With no host remark, the config name is the inbound's own remark.
+	// With no host remark, the config name is likewise the inbound's own remark.
 	if got := s.genHostRemark(inbound, client, ""); got != "DE" {
 	if got := s.genHostRemark(inbound, client, ""); got != "DE" {
 		t.Fatalf("display host link (no host) = %q, want %q", got, "DE")
 		t.Fatalf("display host link (no host) = %q, want %q", got, "DE")
 	}
 	}
@@ -270,6 +270,26 @@ func TestNameOnlyTemplate(t *testing.T) {
 	}
 	}
 }
 }
 
 
+// statsForClient resolves usage from the per-request statsByEmail map when the
+// link's own inbound doesn't carry the client's (globally unique) traffic row —
+// the multi-inbound case that made {{TRAFFIC_LEFT}} show the full quota (#5443).
+func TestStatsForClient_CrossInboundFallback(t *testing.T) {
+	s := &SubService{
+		statsByEmail: map[string]xray.ClientTraffic{
+			"[email protected]": {Email: "[email protected]", Total: 100 * gb, Up: 15 * gb, Down: 5 * gb},
+		},
+	}
+	// Inbound B carries no ClientStats for john (his row is owned by inbound A).
+	inboundB := &model.Inbound{Remark: "B"}
+	st := s.statsForClient(inboundB, model.Client{Email: "[email protected]"})
+	if used := st.Up + st.Down; used != 20*gb {
+		t.Fatalf("statsForClient used = %d, want %d (cross-inbound fallback)", used, 20*gb)
+	}
+	if got := remarkVarValue("TRAFFIC_LEFT", remarkContext{stats: st}); got != "80.00GB" {
+		t.Fatalf("TRAFFIC_LEFT = %q, want 80.00GB (remaining, not total)", got)
+	}
+}
+
 // Two clients through the same global template get distinct, per-client remarks.
 // Two clients through the same global template get distinct, per-client remarks.
 func TestGenHostRemark_PerClient(t *testing.T) {
 func TestGenHostRemark_PerClient(t *testing.T) {
 	s := &SubService{remarkTemplate: "{{EMAIL}}", subscriptionBody: true}
 	s := &SubService{remarkTemplate: "{{EMAIL}}", subscriptionBody: true}

+ 22 - 0
internal/sub/service.go

@@ -47,6 +47,12 @@ type SubService struct {
 	// inbound whose NodeID is set. Keeps the per-link host derivation
 	// inbound whose NodeID is set. Keeps the per-link host derivation
 	// O(1) instead of O(N) DB hits.
 	// O(1) instead of O(N) DB hits.
 	nodesByID map[int]*model.Node
 	nodesByID map[int]*model.Node
+	// statsByEmail maps a client email to its traffic row across ALL inbounds
+	// loaded for the request. client_traffics.email is globally unique, so this
+	// lets statsForClient resolve usage for a client even on an inbound that
+	// doesn't own its row (multi-inbound subscriptions). Filled in
+	// getInboundsBySubId; reset per request in PrepareForRequest.
+	statsByEmail map[string]xray.ClientTraffic
 }
 }
 
 
 // NewSubService creates a new subscription service with the given configuration.
 // NewSubService creates a new subscription service with the given configuration.
@@ -78,6 +84,7 @@ func (s *SubService) PrepareForRequest(host string) {
 	}
 	}
 	s.address = host
 	s.address = host
 	s.usageShown = map[string]bool{}
 	s.usageShown = map[string]bool{}
+	s.statsByEmail = map[string]xray.ClientTraffic{}
 	s.loadNodes()
 	s.loadNodes()
 	s.loadRemarkSettings()
 	s.loadRemarkSettings()
 }
 }
@@ -335,9 +342,24 @@ func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+	s.indexStatsByEmail(inbounds)
 	return inbounds, nil
 	return inbounds, nil
 }
 }
 
 
+// indexStatsByEmail records every loaded inbound's client traffic rows keyed by
+// email so statsForClient can resolve a client's usage even on an inbound that
+// doesn't own its (globally unique) client_traffics row. See statsByEmail.
+func (s *SubService) indexStatsByEmail(inbounds []*model.Inbound) {
+	if s.statsByEmail == nil {
+		s.statsByEmail = map[string]xray.ClientTraffic{}
+	}
+	for _, inbound := range inbounds {
+		for _, st := range inbound.ClientStats {
+			s.statsByEmail[st.Email] = st
+		}
+	}
+}
+
 // projectThroughFallbackMaster mutates the inbound in place so its
 // projectThroughFallbackMaster mutates the inbound in place so its
 // Listen/Port/StreamSettings reflect the externally reachable master
 // Listen/Port/StreamSettings reflect the externally reachable master
 // when applicable. Covers both fallback mechanisms:
 // when applicable. Covers both fallback mechanisms:

+ 1 - 1
internal/web/translation/ar-EG.json

@@ -1761,7 +1761,7 @@
           "time": "الوقت والحالة"
           "time": "الوقت والحالة"
         },
         },
         "descEMAIL": "بريد العميل",
         "descEMAIL": "بريد العميل",
-        "descINBOUND": "اسم الإعداد: ملاحظة المضيف عند تعيينها، وإلا ملاحظة الوارد",
+        "descINBOUND": "ملاحظة الوارد نفسه (اسم الإعداد)",
         "descHOST": "ملاحظة المضيف",
         "descHOST": "ملاحظة المضيف",
         "descID": "UUID العميل",
         "descID": "UUID العميل",
         "descSHORT_ID": "أول 8 أحرف من الـ UUID",
         "descSHORT_ID": "أول 8 أحرف من الـ UUID",

+ 1 - 1
internal/web/translation/en-US.json

@@ -951,7 +951,7 @@
           "time": "Time & status"
           "time": "Time & status"
         },
         },
         "descEMAIL": "Client email",
         "descEMAIL": "Client email",
-        "descINBOUND": "Config name: the host's remark when set, otherwise the inbound's remark",
+        "descINBOUND": "Inbound's own remark (the config name)",
         "descHOST": "Host remark",
         "descHOST": "Host remark",
         "descID": "Client UUID",
         "descID": "Client UUID",
         "descSHORT_ID": "First 8 characters of the UUID",
         "descSHORT_ID": "First 8 characters of the UUID",

+ 1 - 1
internal/web/translation/es-ES.json

@@ -1761,7 +1761,7 @@
           "time": "Tiempo y estado"
           "time": "Tiempo y estado"
         },
         },
         "descEMAIL": "Email del cliente",
         "descEMAIL": "Email del cliente",
-        "descINBOUND": "Nombre de la configuración: las notas del host cuando están definidas, de lo contrario las notas del inbound",
+        "descINBOUND": "Notas del propio inbound (nombre de la configuración)",
         "descHOST": "Notas del host",
         "descHOST": "Notas del host",
         "descID": "UUID del cliente",
         "descID": "UUID del cliente",
         "descSHORT_ID": "Primeros 8 caracteres del UUID",
         "descSHORT_ID": "Primeros 8 caracteres del UUID",

+ 1 - 1
internal/web/translation/fa-IR.json

@@ -1761,7 +1761,7 @@
           "time": "زمان و وضعیت"
           "time": "زمان و وضعیت"
         },
         },
         "descEMAIL": "ایمیل کاربر",
         "descEMAIL": "ایمیل کاربر",
-        "descINBOUND": "نام کانفیگ: نام میزبان در صورت تنظیم، در غیر این صورت نام اینباند",
+        "descINBOUND": "نام خود اینباند (نام کانفیگ)",
         "descHOST": "نام میزبان",
         "descHOST": "نام میزبان",
         "descID": "UUID کاربر",
         "descID": "UUID کاربر",
         "descSHORT_ID": "۸ کاراکتر اول UUID",
         "descSHORT_ID": "۸ کاراکتر اول UUID",

+ 1 - 1
internal/web/translation/id-ID.json

@@ -1761,7 +1761,7 @@
           "time": "Waktu & status"
           "time": "Waktu & status"
         },
         },
         "descEMAIL": "Email klien",
         "descEMAIL": "Email klien",
-        "descINBOUND": "Nama konfigurasi: catatan host bila diatur, jika tidak catatan inbound",
+        "descINBOUND": "Catatan inbound itu sendiri (nama konfigurasi)",
         "descHOST": "Catatan host",
         "descHOST": "Catatan host",
         "descID": "UUID klien",
         "descID": "UUID klien",
         "descSHORT_ID": "8 karakter pertama dari UUID",
         "descSHORT_ID": "8 karakter pertama dari UUID",

+ 1 - 1
internal/web/translation/ja-JP.json

@@ -1761,7 +1761,7 @@
           "time": "時刻とステータス"
           "time": "時刻とステータス"
         },
         },
         "descEMAIL": "クライアントのメール",
         "descEMAIL": "クライアントのメール",
-        "descINBOUND": "設定名: ホストの備考が設定されている場合はそれ、それ以外はインバウンドの備考",
+        "descINBOUND": "インバウンド自身の備考(設定名)",
         "descHOST": "ホストの備考",
         "descHOST": "ホストの備考",
         "descID": "クライアント UUID",
         "descID": "クライアント UUID",
         "descSHORT_ID": "UUID の最初の 8 文字",
         "descSHORT_ID": "UUID の最初の 8 文字",

+ 1 - 1
internal/web/translation/pt-BR.json

@@ -1761,7 +1761,7 @@
           "time": "Tempo e status"
           "time": "Tempo e status"
         },
         },
         "descEMAIL": "Email do cliente",
         "descEMAIL": "Email do cliente",
-        "descINBOUND": "Nome da configuração: a observação do host quando definida, caso contrário a observação da entrada",
+        "descINBOUND": "Observação da própria entrada (nome da configuração)",
         "descHOST": "Observação do host",
         "descHOST": "Observação do host",
         "descID": "UUID do cliente",
         "descID": "UUID do cliente",
         "descSHORT_ID": "Primeiros 8 caracteres do UUID",
         "descSHORT_ID": "Primeiros 8 caracteres do UUID",

+ 1 - 1
internal/web/translation/ru-RU.json

@@ -1761,7 +1761,7 @@
           "time": "Время и статус"
           "time": "Время и статус"
         },
         },
         "descEMAIL": "Email клиента",
         "descEMAIL": "Email клиента",
-        "descINBOUND": "Имя конфигурации: примечание хоста, если задано, иначе примечание входящего",
+        "descINBOUND": "Собственное примечание входящего (имя конфигурации)",
         "descHOST": "Примечание хоста",
         "descHOST": "Примечание хоста",
         "descID": "UUID клиента",
         "descID": "UUID клиента",
         "descSHORT_ID": "Первые 8 символов UUID",
         "descSHORT_ID": "Первые 8 символов UUID",

+ 1 - 1
internal/web/translation/tr-TR.json

@@ -1761,7 +1761,7 @@
           "time": "Zaman ve durum"
           "time": "Zaman ve durum"
         },
         },
         "descEMAIL": "Kullanıcı e-postası",
         "descEMAIL": "Kullanıcı e-postası",
-        "descINBOUND": "Yapılandırma adı: ayarlanmışsa host'un açıklaması, aksi halde gelen bağlantının açıklaması",
+        "descINBOUND": "Gelen bağlantının kendi açıklaması (yapılandırma adı)",
         "descHOST": "Host açıklaması",
         "descHOST": "Host açıklaması",
         "descID": "Kullanıcı UUID'si",
         "descID": "Kullanıcı UUID'si",
         "descSHORT_ID": "UUID'nin ilk 8 karakteri",
         "descSHORT_ID": "UUID'nin ilk 8 karakteri",

+ 1 - 1
internal/web/translation/uk-UA.json

@@ -1761,7 +1761,7 @@
           "time": "Час і статус"
           "time": "Час і статус"
         },
         },
         "descEMAIL": "Email клієнта",
         "descEMAIL": "Email клієнта",
-        "descINBOUND": "Назва конфігурації: примітка хоста, якщо задана, інакше примітка вхідного",
+        "descINBOUND": "Власна примітка вхідного (назва конфігурації)",
         "descHOST": "Примітка хоста",
         "descHOST": "Примітка хоста",
         "descID": "UUID клієнта",
         "descID": "UUID клієнта",
         "descSHORT_ID": "Перші 8 символів UUID",
         "descSHORT_ID": "Перші 8 символів UUID",

+ 1 - 1
internal/web/translation/vi-VN.json

@@ -1761,7 +1761,7 @@
           "time": "Thời gian & trạng thái"
           "time": "Thời gian & trạng thái"
         },
         },
         "descEMAIL": "Email khách hàng",
         "descEMAIL": "Email khách hàng",
-        "descINBOUND": "Tên cấu hình: ghi chú của host nếu được đặt, nếu không thì ghi chú của inbound",
+        "descINBOUND": "Ghi chú của chính inbound (tên cấu hình)",
         "descHOST": "Ghi chú host",
         "descHOST": "Ghi chú host",
         "descID": "UUID khách hàng",
         "descID": "UUID khách hàng",
         "descSHORT_ID": "8 ký tự đầu của UUID",
         "descSHORT_ID": "8 ký tự đầu của UUID",

+ 1 - 1
internal/web/translation/zh-CN.json

@@ -1761,7 +1761,7 @@
           "time": "时间与状态"
           "time": "时间与状态"
         },
         },
         "descEMAIL": "客户端邮箱",
         "descEMAIL": "客户端邮箱",
-        "descINBOUND": "配置名称:已设置时为主机的备注,否则为入站的备注",
+        "descINBOUND": "入站本身的备注(配置名称)",
         "descHOST": "主机备注",
         "descHOST": "主机备注",
         "descID": "客户端 UUID",
         "descID": "客户端 UUID",
         "descSHORT_ID": "UUID 的前 8 个字符",
         "descSHORT_ID": "UUID 的前 8 个字符",

+ 1 - 1
internal/web/translation/zh-TW.json

@@ -1761,7 +1761,7 @@
           "time": "時間與狀態"
           "time": "時間與狀態"
         },
         },
         "descEMAIL": "客戶端電子郵件",
         "descEMAIL": "客戶端電子郵件",
-        "descINBOUND": "配置名稱:設定時為 Host 的備註,否則為入站的備註",
+        "descINBOUND": "入站本身的備註(配置名稱)",
         "descHOST": "Host 備註",
         "descHOST": "Host 備註",
         "descID": "客戶端 UUID",
         "descID": "客戶端 UUID",
         "descSHORT_ID": "UUID 的前 8 個字元",
         "descSHORT_ID": "UUID 的前 8 個字元",