mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
feat(sub): add dummy info node and status configs for subscriptions (#6412)
* feat(settings): add subInfoNodeEnable and status template settings * feat(sub): add dummy info node and status configs for raw links * feat(sub): support dummy info node in clash and json subscriptions * feat(ui): add subscription info node switch and status templates to settings * style: apply gofumpt formatting * fix(sub): address review feedback on subscription info node - Restore GetSubs contract to avoid unintended remark expansions on non-subscription-body calls. - Exclude dummy info node from Clash PROXY select group when active server nodes exist. - Track hasEnabledClient and set traffic.Enable in JSON and Clash paths so status tokens evaluate correctly. - Deterministically sort client emails across subscriptions before selecting primaryEmail. - Consolidate duplicated info-node evaluation logic into resolveInfoNodeRemark helper. - Remove redundant pure-getter test from setting_sub_info_node_test.go. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -30,12 +30,15 @@ type AllSetting struct {
|
||||
IpLimitAllowlist string `json:"ipLimitAllowlist" form:"ipLimitAllowlist"`
|
||||
PanelOutbound string `json:"panelOutbound" form:"panelOutbound"`
|
||||
|
||||
PageSize int `json:"pageSize" form:"pageSize" validate:"gte=0,lte=1000"`
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff" validate:"gte=0"`
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"`
|
||||
RemarkTemplate string `json:"remarkTemplate" form:"remarkTemplate"`
|
||||
SubShowIdentityOnAllLinks bool `json:"subShowIdentityOnAllLinks" form:"subShowIdentityOnAllLinks"`
|
||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
||||
PageSize int `json:"pageSize" form:"pageSize" validate:"gte=0,lte=1000"`
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff" validate:"gte=0"`
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"`
|
||||
RemarkTemplate string `json:"remarkTemplate" form:"remarkTemplate"`
|
||||
SubShowIdentityOnAllLinks bool `json:"subShowIdentityOnAllLinks" form:"subShowIdentityOnAllLinks"`
|
||||
SubInfoNodeEnable bool `json:"subInfoNodeEnable" form:"subInfoNodeEnable"`
|
||||
SubExpiredTemplate string `json:"subExpiredTemplate" form:"subExpiredTemplate"`
|
||||
SubTrafficDepletedTemplate string `json:"subTrafficDepletedTemplate" form:"subTrafficDepletedTemplate"`
|
||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
||||
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
|
||||
@@ -34,11 +34,13 @@ import (
|
||||
var xrayTemplateConfig string
|
||||
|
||||
const (
|
||||
DefaultSubClashUserAgentRegex = `(?i)(clash|mihomo)`
|
||||
DefaultSubJsonUserAgentRegex = ``
|
||||
DefaultRemarkTemplate = "{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D"
|
||||
DefaultTrustedProxyCIDRs = "127.0.0.1/32,::1/128"
|
||||
maxRegexLength = 2048
|
||||
DefaultSubClashUserAgentRegex = `(?i)(clash|mihomo)`
|
||||
DefaultSubJsonUserAgentRegex = ``
|
||||
DefaultRemarkTemplate = "{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D"
|
||||
DefaultSubExpiredTemplate = "⛔ {{EMAIL}} | Expired: {{EXPIRE_DATE}}"
|
||||
DefaultSubTrafficDepletedTemplate = "🚫 {{EMAIL}} | Traffic Depleted | {{TRAFFIC_USED}}/{{TRAFFIC_TOTAL}}"
|
||||
DefaultTrustedProxyCIDRs = "127.0.0.1/32,::1/128"
|
||||
maxRegexLength = 2048
|
||||
)
|
||||
|
||||
var defaultValueMap = map[string]string{
|
||||
@@ -70,6 +72,9 @@ var defaultValueMap = map[string]string{
|
||||
"trafficDiff": "0",
|
||||
"remarkTemplate": DefaultRemarkTemplate,
|
||||
"subShowIdentityOnAllLinks": "false",
|
||||
"subInfoNodeEnable": "false",
|
||||
"subExpiredTemplate": DefaultSubExpiredTemplate,
|
||||
"subTrafficDepletedTemplate": DefaultSubTrafficDepletedTemplate,
|
||||
"timeLocation": "Local",
|
||||
"tgBotEnable": "false",
|
||||
"tgBotToken": "",
|
||||
@@ -676,6 +681,18 @@ func (s *SettingService) GetSubShowIdentityOnAllLinks() (bool, error) {
|
||||
return s.getBool("subShowIdentityOnAllLinks")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubInfoNodeEnable() (bool, error) {
|
||||
return s.getBool("subInfoNodeEnable")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubExpiredTemplate() (string, error) {
|
||||
return s.getString("subExpiredTemplate")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubTrafficDepletedTemplate() (string, error) {
|
||||
return s.getString("subTrafficDepletedTemplate")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
secret, err := s.getString("secret")
|
||||
if secret == "" || secret == defaultValueMap["secret"] {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSubInfoNodeSettingsDefaultsAndPersists(t *testing.T) {
|
||||
setupSettingTestDB(t)
|
||||
s := &SettingService{}
|
||||
|
||||
settings, err := s.GetAllSetting()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.SubInfoNodeEnable {
|
||||
t.Fatal("expected default SubInfoNodeEnable false")
|
||||
}
|
||||
if settings.SubExpiredTemplate != DefaultSubExpiredTemplate {
|
||||
t.Fatalf("expected default SubExpiredTemplate %q, got %q", DefaultSubExpiredTemplate, settings.SubExpiredTemplate)
|
||||
}
|
||||
if settings.SubTrafficDepletedTemplate != DefaultSubTrafficDepletedTemplate {
|
||||
t.Fatalf("expected default SubTrafficDepletedTemplate %q, got %q", DefaultSubTrafficDepletedTemplate, settings.SubTrafficDepletedTemplate)
|
||||
}
|
||||
|
||||
settings.SubInfoNodeEnable = true
|
||||
settings.SubExpiredTemplate = "custom expired"
|
||||
settings.SubTrafficDepletedTemplate = "custom depleted"
|
||||
if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotEnabled, err := s.GetSubInfoNodeEnable()
|
||||
if err != nil || !gotEnabled {
|
||||
t.Fatalf("expected true, got %v, err %v", gotEnabled, err)
|
||||
}
|
||||
gotExp, err := s.GetSubExpiredTemplate()
|
||||
if err != nil || gotExp != "custom expired" {
|
||||
t.Fatalf("expected 'custom expired', got %q, err %v", gotExp, err)
|
||||
}
|
||||
gotDep, err := s.GetSubTrafficDepletedTemplate()
|
||||
if err != nil || gotDep != "custom depleted" {
|
||||
t.Fatalf("expected 'custom depleted', got %q, err %v", gotDep, err)
|
||||
}
|
||||
}
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "الأسلوب المستخدم في طلبات probe.",
|
||||
"note": "تحمل موزّعات leastPing/leastLoad دائمًا burstObservatory. يخصّص هذا المفتاح معاملات probe — أوقفه لاستخدام الإعدادات الافتراضية المدمجة. تُطبَّق التغييرات بعد إعادة تشغيل اللوحة."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "تكوين معلومات منفصل / عقدة وهمية",
|
||||
"subInfoNodeEnableDesc": "عند التفعيل، يتم عرض الملاحظات وحركة المرور/الأيام المتبقية كتكوين SOCKS منفصل في الأعلى، وعند انتهاء الصلاحية أو نفاد البيانات يتم إرجاع تكوين الحالة فقط.",
|
||||
"subExpiredTemplate": "قالب انتهاء الصلاحية",
|
||||
"subExpiredTemplateDesc": "قالب التكوين الوهمي عند انتهاء صلاحية اشتراك المستخدم.",
|
||||
"subTrafficDepletedTemplate": "قالب نفاد البيانات",
|
||||
"subTrafficDepletedTemplateDesc": "قالب التكوين الوهمي عند استهلاك حصة بيانات المستخدم بالكامل."
|
||||
},
|
||||
"xray": {
|
||||
"save": "احفظ",
|
||||
|
||||
@@ -1574,7 +1574,13 @@
|
||||
"httpMethodDesc": "Method used for probe requests.",
|
||||
"note": "leastPing/leastLoad balancers always carry a burst observatory. This switch customises its probe parameters — turn it off to use the built-in defaults. Changes apply after a panel restart."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "Separate Info Config / Dummy Node",
|
||||
"subInfoNodeEnableDesc": "When enabled, remarks and remaining traffic/days are served as a separate dummy SOCKS config at the top, and expired/depleted subscriptions return only the status config.",
|
||||
"subExpiredTemplate": "Expired Template",
|
||||
"subExpiredTemplateDesc": "Template for the dummy config when the subscriber account has expired.",
|
||||
"subTrafficDepletedTemplate": "Traffic Depleted Template",
|
||||
"subTrafficDepletedTemplateDesc": "Template for the dummy config when subscriber traffic quota is exhausted."
|
||||
},
|
||||
"xray": {
|
||||
"save": "Save",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "Método usado para las solicitudes de probe.",
|
||||
"note": "Los balanceadores leastPing/leastLoad siempre llevan un burstObservatory. Este interruptor personaliza sus parámetros de probe — apágalo para usar los valores predeterminados integrados. Los cambios se aplican tras reiniciar el panel."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "Configuración de información separada / Nodo ficticio",
|
||||
"subInfoNodeEnableDesc": "Si está activado, la información de tráfico y días restantes se muestra en un nodo SOCKS ficticio arriba, y las suscripciones caducadas o agotadas devuelven solo el estado.",
|
||||
"subExpiredTemplate": "Plantilla de expirado",
|
||||
"subExpiredTemplateDesc": "Plantilla para el nodo ficticio cuando la suscripción ha expirado.",
|
||||
"subTrafficDepletedTemplate": "Plantilla de tráfico agotado",
|
||||
"subTrafficDepletedTemplateDesc": "Plantilla para el nodo ficticio cuando el límite de tráfico se ha agotado."
|
||||
},
|
||||
"xray": {
|
||||
"save": "Guardar configuración",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "متد استفادهشده برای درخواستهای probe.",
|
||||
"note": "موزانکنندههای leastPing/leastLoad همیشه burstObservatory دارند. این کلید پارامترهای probe آن را سفارشی میکند — آن را خاموش کنید تا از پیشفرضهای داخلی استفاده شود. تغییرات پس از راهاندازی مجدد پنل اعمال میشوند."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "کانفیگ جداگانه اطلاعات / نود نمایشی",
|
||||
"subInfoNodeEnableDesc": "در صورت فعال بودن، مشخصات و حجم/روزهای باقیمانده به عنوان یک کانفیگ مجزای ساکس در بالای لیست نمایش داده میشود و در صورت انقضا یا اتمام حجم فقط پیام وضعیت ارسال میشود.",
|
||||
"subExpiredTemplate": "قالب پیام انقضا",
|
||||
"subExpiredTemplateDesc": "قالب کانفیگ نمایشی زمانی که اشتراک کاربر منقضی شده است.",
|
||||
"subTrafficDepletedTemplate": "قالب پیام اتمام حجم",
|
||||
"subTrafficDepletedTemplateDesc": "قالب کانفیگ نمایشی زمانی که حجم اشتراک کاربر به پایان رسیده است."
|
||||
},
|
||||
"xray": {
|
||||
"save": "ذخیره",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "Metode yang dipakai untuk permintaan probe.",
|
||||
"note": "Penyeimbang leastPing/leastLoad selalu membawa burstObservatory. Sakelar ini menyesuaikan parameter probe-nya — matikan untuk memakai bawaan default. Perubahan berlaku setelah panel dimulai ulang."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "Konfigurasi Info Terpisah / Node Dummy",
|
||||
"subInfoNodeEnableDesc": "Saat diaktifkan, catatan dan sisa kuota/hari disajikan sebagai konfigurasi SOCKS dummy terpisah di bagian atas, dan langganan kedaluwarsa/habis hanya mengembalikan status.",
|
||||
"subExpiredTemplate": "Templat Kedaluwarsa",
|
||||
"subExpiredTemplateDesc": "Templat untuk konfigurasi dummy saat akun langganan telah kedaluwarsa.",
|
||||
"subTrafficDepletedTemplate": "Templat Kuota Habis",
|
||||
"subTrafficDepletedTemplateDesc": "Templat untuk konfigurasi dummy saat kuota data langganan telah habis."
|
||||
},
|
||||
"xray": {
|
||||
"save": "Simpan",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "プローブ要求に使う HTTP メソッド。",
|
||||
"note": "leastPing/leastLoad バランサーは常に burstObservatory を持ちます。このスイッチはプローブパラメータをカスタマイズします — オフにすると組み込みのデフォルトを使います。変更はパネルの再起動後に反映されます。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "個別情報設定 / ダミーノード",
|
||||
"subInfoNodeEnableDesc": "有効にすると、備考と残りの通信量/日数が上部の独立したSOCKSダミー構成として表示され、期限切れや通信量超過時はステータスのみが返されます。",
|
||||
"subExpiredTemplate": "期限切れテンプレート",
|
||||
"subExpiredTemplateDesc": "サブスクリプションの有効期限が切れた際のダミー構成用テンプレート。",
|
||||
"subTrafficDepletedTemplate": "通信量超過テンプレート",
|
||||
"subTrafficDepletedTemplateDesc": "サブスクリプションの通信量が上限に達した際のダミー構成用テンプレート。"
|
||||
},
|
||||
"xray": {
|
||||
"importRules": "ルールをインポート",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "Método usado nas requisições de probe.",
|
||||
"note": "Balanceadores leastPing/leastLoad sempre carregam um burstObservatory. Esta opção personaliza seus parâmetros de probe — desligue-a para usar os padrões integrados. As alterações se aplicam após reiniciar o painel."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "Configuração de informações separada / Nó fictício",
|
||||
"subInfoNodeEnableDesc": "Quando ativado, observações e tráfego/dias restantes são exibidos em uma configuração SOCKS fictícia no topo, e assinaturas expiradas/esgotadas retornam apenas o status.",
|
||||
"subExpiredTemplate": "Modelo expirado",
|
||||
"subExpiredTemplateDesc": "Modelo para a configuração fictícia quando a assinatura expirou.",
|
||||
"subTrafficDepletedTemplate": "Modelo de tráfego esgotado",
|
||||
"subTrafficDepletedTemplateDesc": "Modelo para a configuração fictícia quando a cota de tráfego foi esgotada."
|
||||
},
|
||||
"xray": {
|
||||
"importRules": "Importar regras",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "Метод запросов при проверках.",
|
||||
"note": "Балансировщики leastPing/leastLoad всегда содержат burst-обсерваторию. Этот переключатель настраивает её параметры проб — выключите, чтобы использовать встроенные значения по умолчанию. Изменения применяются после перезапуска панели."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "Отдельный инфо-конфиг / фиктивный узел",
|
||||
"subInfoNodeEnableDesc": "Если включено, примечание и остаток трафика/дней отображаются отдельным SOCKS-конфигом вверху, а при истечении срока/трафика возвращается только статус.",
|
||||
"subExpiredTemplate": "Шаблон истекшей подписки",
|
||||
"subExpiredTemplateDesc": "Шаблон для фиктивного узла, когда срок действия подписки истек.",
|
||||
"subTrafficDepletedTemplate": "Шаблон исчерпания трафика",
|
||||
"subTrafficDepletedTemplateDesc": "Шаблон для фиктивного узла, когда лимит трафика исчерпан."
|
||||
},
|
||||
"xray": {
|
||||
"importRules": "Импорт правил",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "Probe isteklerinde kullanılan HTTP yöntemi.",
|
||||
"note": "leastPing/leastLoad dengeleyicileri her zaman bir burstObservatory taşır. Bu anahtar probe parametrelerini özelleştirir — yerleşik varsayılanları kullanmak için kapatın. Değişiklikler panel yeniden başlatıldıktan sonra uygulanır."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "Ayrı Bilgi Yapılandırması / Sahte Düğüm",
|
||||
"subInfoNodeEnableDesc": "Etkinleştirildiğinde, notlar ve kalan trafik/günler en üstte ayrı bir sahte SOCKS yapılandırması olarak sunulur; süresi dolmuş veya kotası bitmiş abonelikler yalnızca durum yapılandırmasını alır.",
|
||||
"subExpiredTemplate": "Süresi Dolmuş Şablonu",
|
||||
"subExpiredTemplateDesc": "Abonelik süresi dolduğunda sahte yapılandırma için kullanılacak şablon.",
|
||||
"subTrafficDepletedTemplate": "Trafik Tükendi Şablonu",
|
||||
"subTrafficDepletedTemplateDesc": "Abonelik trafik kotası bittiğinde sahte yapılandırma için kullanılacak şablon."
|
||||
},
|
||||
"xray": {
|
||||
"save": "Kaydet",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "Метод запитів під час перевірок.",
|
||||
"note": "Балансувальники leastPing/leastLoad завжди мають burstObservatory. Цей перемикач налаштовує її параметри probe — вимкніть, щоб використовувати вбудовані значення за замовчуванням. Зміни застосовуються після перезапуску панелі."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "Окремий інфо-конфіг / фіктивний вузол",
|
||||
"subInfoNodeEnableDesc": "Якщо увімкнено, примітка та залишок трафіку/днів відображаються окремим SOCKS-конфігом угорі, а після закінчення терміну/трафіку повертається лише статус.",
|
||||
"subExpiredTemplate": "Шаблон закінчення терміну",
|
||||
"subExpiredTemplateDesc": "Шаблон для фіктивного вузла, коли термін дії підписки закінчився.",
|
||||
"subTrafficDepletedTemplate": "Шаблон вичерпання трафіку",
|
||||
"subTrafficDepletedTemplateDesc": "Шаблон для фіктивного вузла, коли ліміт трафіку вичерпано."
|
||||
},
|
||||
"xray": {
|
||||
"save": "Зберегти",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "Phương thức dùng cho yêu cầu probe.",
|
||||
"note": "Các bộ cân bằng leastPing/leastLoad luôn mang một burstObservatory. Công tắc này tùy chỉnh các tham số probe — tắt nó để dùng mặc định tích hợp. Các thay đổi áp dụng sau khi khởi động lại bảng điều khiển."
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "Cấu hình thông tin riêng / Nút ảo",
|
||||
"subInfoNodeEnableDesc": "Khi bật, ghi chú và lưu lượng/ngày còn lại sẽ hiển thị dưới dạng cấu hình SOCKS ảo riêng ở trên cùng, và đăng ký hết hạn/hết dung lượng chỉ trả về cấu hình trạng thái.",
|
||||
"subExpiredTemplate": "Mẫu hết hạn",
|
||||
"subExpiredTemplateDesc": "Mẫu cho cấu hình ảo khi tài khoản đăng ký đã hết hạn.",
|
||||
"subTrafficDepletedTemplate": "Mẫu hết dung lượng",
|
||||
"subTrafficDepletedTemplateDesc": "Mẫu cho cấu hình ảo khi tài khoản đăng ký đã dùng hết dung lượng."
|
||||
},
|
||||
"xray": {
|
||||
"importRules": "Nhập quy tắc",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "探活请求使用的 HTTP 方法。",
|
||||
"note": "leastPing/leastLoad 均衡器始终带有 burstObservatory。此开关自定义其探活参数 — 关闭以使用内置默认值。更改在面板重启后生效。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "独立信息配置 / 提示节点",
|
||||
"subInfoNodeEnableDesc": "启用后,备注及剩余流量/天数将作为单独的 SOCKS 提示配置显示在顶部;过期或流量耗尽时仅返回状态提示。",
|
||||
"subExpiredTemplate": "过期提示模板",
|
||||
"subExpiredTemplateDesc": "用户订阅过期时提示配置的备注模板。",
|
||||
"subTrafficDepletedTemplate": "流量耗尽模板",
|
||||
"subTrafficDepletedTemplateDesc": "用户订阅流量用尽时提示配置的备注模板。"
|
||||
},
|
||||
"xray": {
|
||||
"importRules": "导入规则",
|
||||
|
||||
@@ -1456,7 +1456,13 @@
|
||||
"httpMethodDesc": "探活請求使用的 HTTP 方法。",
|
||||
"note": "leastPing/leastLoad 平衡器始終帶有 burstObservatory。此開關自訂其探活參數 — 關閉以使用內建預設值。變更在面板重啟後生效。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subInfoNodeEnable": "獨立資訊配置 / 提示節點",
|
||||
"subInfoNodeEnableDesc": "啟用後,備註及剩餘流量/天數將作為單獨的 SOCKS 提示配置顯示在頂部;過期或流量耗盡時僅返回狀態提示。",
|
||||
"subExpiredTemplate": "過期提示範本",
|
||||
"subExpiredTemplateDesc": "使用者訂閱過期時提示配置的備註範本。",
|
||||
"subTrafficDepletedTemplate": "流量耗盡範本",
|
||||
"subTrafficDepletedTemplateDesc": "使用者訂閱流量用盡時提示配置的備註範本。"
|
||||
},
|
||||
"xray": {
|
||||
"save": "儲存",
|
||||
|
||||
Reference in New Issue
Block a user