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
+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)
}
}