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:
Rouzbeh†
2026-09-08 18:44:01 +03:30
committed by GitHub
parent b8597314f8
commit 9f76a66dcf
33 changed files with 1374 additions and 35 deletions
+9 -6
View File
@@ -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"`
+22 -5
View File
@@ -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)
}
}
+7 -1
View File
@@ -1456,7 +1456,13 @@
"httpMethodDesc": "الأسلوب المستخدم في طلبات probe.",
"note": "تحمل موزّعات leastPing/leastLoad دائمًا burstObservatory. يخصّص هذا المفتاح معاملات probe — أوقفه لاستخدام الإعدادات الافتراضية المدمجة. تُطبَّق التغييرات بعد إعادة تشغيل اللوحة."
}
}
},
"subInfoNodeEnable": "تكوين معلومات منفصل / عقدة وهمية",
"subInfoNodeEnableDesc": "عند التفعيل، يتم عرض الملاحظات وحركة المرور/الأيام المتبقية كتكوين SOCKS منفصل في الأعلى، وعند انتهاء الصلاحية أو نفاد البيانات يتم إرجاع تكوين الحالة فقط.",
"subExpiredTemplate": "قالب انتهاء الصلاحية",
"subExpiredTemplateDesc": "قالب التكوين الوهمي عند انتهاء صلاحية اشتراك المستخدم.",
"subTrafficDepletedTemplate": "قالب نفاد البيانات",
"subTrafficDepletedTemplateDesc": "قالب التكوين الوهمي عند استهلاك حصة بيانات المستخدم بالكامل."
},
"xray": {
"save": "احفظ",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -1456,7 +1456,13 @@
"httpMethodDesc": "متد استفاده‌شده برای درخواست‌های probe.",
"note": "موزان‌کننده‌های leastPing/leastLoad همیشه burstObservatory دارند. این کلید پارامترهای probe آن را سفارشی می‌کند — آن را خاموش کنید تا از پیش‌فرض‌های داخلی استفاده شود. تغییرات پس از راه‌اندازی مجدد پنل اعمال می‌شوند."
}
}
},
"subInfoNodeEnable": "کانفیگ جداگانه اطلاعات / نود نمایشی",
"subInfoNodeEnableDesc": "در صورت فعال بودن، مشخصات و حجم/روزهای باقیمانده به عنوان یک کانفیگ مجزای ساکس در بالای لیست نمایش داده می‌شود و در صورت انقضا یا اتمام حجم فقط پیام وضعیت ارسال می‌شود.",
"subExpiredTemplate": "قالب پیام انقضا",
"subExpiredTemplateDesc": "قالب کانفیگ نمایشی زمانی که اشتراک کاربر منقضی شده است.",
"subTrafficDepletedTemplate": "قالب پیام اتمام حجم",
"subTrafficDepletedTemplateDesc": "قالب کانفیگ نمایشی زمانی که حجم اشتراک کاربر به پایان رسیده است."
},
"xray": {
"save": "ذخیره",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -1456,7 +1456,13 @@
"httpMethodDesc": "プローブ要求に使う HTTP メソッド。",
"note": "leastPing/leastLoad バランサーは常に burstObservatory を持ちます。このスイッチはプローブパラメータをカスタマイズします — オフにすると組み込みのデフォルトを使います。変更はパネルの再起動後に反映されます。"
}
}
},
"subInfoNodeEnable": "個別情報設定 / ダミーノード",
"subInfoNodeEnableDesc": "有効にすると、備考と残りの通信量/日数が上部の独立したSOCKSダミー構成として表示され、期限切れや通信量超過時はステータスのみが返されます。",
"subExpiredTemplate": "期限切れテンプレート",
"subExpiredTemplateDesc": "サブスクリプションの有効期限が切れた際のダミー構成用テンプレート。",
"subTrafficDepletedTemplate": "通信量超過テンプレート",
"subTrafficDepletedTemplateDesc": "サブスクリプションの通信量が上限に達した際のダミー構成用テンプレート。"
},
"xray": {
"importRules": "ルールをインポート",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -1456,7 +1456,13 @@
"httpMethodDesc": "Метод запросов при проверках.",
"note": "Балансировщики leastPing/leastLoad всегда содержат burst-обсерваторию. Этот переключатель настраивает её параметры проб — выключите, чтобы использовать встроенные значения по умолчанию. Изменения применяются после перезапуска панели."
}
}
},
"subInfoNodeEnable": "Отдельный инфо-конфиг / фиктивный узел",
"subInfoNodeEnableDesc": "Если включено, примечание и остаток трафика/дней отображаются отдельным SOCKS-конфигом вверху, а при истечении срока/трафика возвращается только статус.",
"subExpiredTemplate": "Шаблон истекшей подписки",
"subExpiredTemplateDesc": "Шаблон для фиктивного узла, когда срок действия подписки истек.",
"subTrafficDepletedTemplate": "Шаблон исчерпания трафика",
"subTrafficDepletedTemplateDesc": "Шаблон для фиктивного узла, когда лимит трафика исчерпан."
},
"xray": {
"importRules": "Импорт правил",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -1456,7 +1456,13 @@
"httpMethodDesc": "Метод запитів під час перевірок.",
"note": "Балансувальники leastPing/leastLoad завжди мають burstObservatory. Цей перемикач налаштовує її параметри probe — вимкніть, щоб використовувати вбудовані значення за замовчуванням. Зміни застосовуються після перезапуску панелі."
}
}
},
"subInfoNodeEnable": "Окремий інфо-конфіг / фіктивний вузол",
"subInfoNodeEnableDesc": "Якщо увімкнено, примітка та залишок трафіку/днів відображаються окремим SOCKS-конфігом угорі, а після закінчення терміну/трафіку повертається лише статус.",
"subExpiredTemplate": "Шаблон закінчення терміну",
"subExpiredTemplateDesc": "Шаблон для фіктивного вузла, коли термін дії підписки закінчився.",
"subTrafficDepletedTemplate": "Шаблон вичерпання трафіку",
"subTrafficDepletedTemplateDesc": "Шаблон для фіктивного вузла, коли ліміт трафіку вичерпано."
},
"xray": {
"save": "Зберегти",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -1456,7 +1456,13 @@
"httpMethodDesc": "探活请求使用的 HTTP 方法。",
"note": "leastPing/leastLoad 均衡器始终带有 burstObservatory。此开关自定义其探活参数 — 关闭以使用内置默认值。更改在面板重启后生效。"
}
}
},
"subInfoNodeEnable": "独立信息配置 / 提示节点",
"subInfoNodeEnableDesc": "启用后,备注及剩余流量/天数将作为单独的 SOCKS 提示配置显示在顶部;过期或流量耗尽时仅返回状态提示。",
"subExpiredTemplate": "过期提示模板",
"subExpiredTemplateDesc": "用户订阅过期时提示配置的备注模板。",
"subTrafficDepletedTemplate": "流量耗尽模板",
"subTrafficDepletedTemplateDesc": "用户订阅流量用尽时提示配置的备注模板。"
},
"xray": {
"importRules": "导入规则",
+7 -1
View File
@@ -1456,7 +1456,13 @@
"httpMethodDesc": "探活請求使用的 HTTP 方法。",
"note": "leastPing/leastLoad 平衡器始終帶有 burstObservatory。此開關自訂其探活參數 — 關閉以使用內建預設值。變更在面板重啟後生效。"
}
}
},
"subInfoNodeEnable": "獨立資訊配置 / 提示節點",
"subInfoNodeEnableDesc": "啟用後,備註及剩餘流量/天數將作為單獨的 SOCKS 提示配置顯示在頂部;過期或流量耗盡時僅返回狀態提示。",
"subExpiredTemplate": "過期提示範本",
"subExpiredTemplateDesc": "使用者訂閱過期時提示配置的備註範本。",
"subTrafficDepletedTemplate": "流量耗盡範本",
"subTrafficDepletedTemplateDesc": "使用者訂閱流量用盡時提示配置的備註範本。"
},
"xray": {
"save": "儲存",