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
+45
View File
@@ -83,6 +83,7 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
var header string
var hasInactiveExternal bool
var hasEnabledClient bool
seenEmails := make(map[string]struct{})
entries := make([]subConfigEntry, 0, len(inbounds))
@@ -99,6 +100,9 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
var inboundConfigs []json_util.RawMessage
for _, client := range clients {
if client.Enable {
hasEnabledClient = true
}
seenEmails[client.Email] = struct{}{}
inboundConfigs = append(inboundConfigs, s.getConfig(subReq, inbound, client, host)...)
}
@@ -128,6 +132,9 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
configArray = append(configArray, entry.configs...)
}
for _, ext := range externalLinks {
if ext.Enable {
hasEnabledClient = true
}
if !ext.Active {
seenEmails[ext.Email] = struct{}{}
hasInactiveExternal = true
@@ -162,8 +169,20 @@ func (s *SubJsonService) GetJson(subId string, host string, alwaysReturnArray bo
for e := range seenEmails {
emails = append(emails, e)
}
slices.Sort(emails)
traffic, _ := subReq.AggregateTrafficByEmails(emails)
traffic.Enable = hasEnabledClient
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
if mode, remark := subReq.resolveInfoNodeRemark(subId, emails, traffic, len(configArray) > 0); mode != infoNodeNone {
dummyConfig := s.genDummySocksConfig(remark)
if mode == infoNodeExpired || mode == infoNodeDepleted {
configArray = []json_util.RawMessage{dummyConfig}
} else {
configArray = append([]json_util.RawMessage{dummyConfig}, configArray...)
}
}
if len(configArray) == 0 {
return "", header, nil
}
@@ -962,6 +981,32 @@ func (s *SubJsonService) genWireguard(inbound *model.Inbound, client model.Clien
return result
}
func (s *SubJsonService) genDummySocksConfig(remark string) json_util.RawMessage {
outbound := map[string]any{
"protocol": "socks",
"tag": "proxy",
"settings": map[string]any{
"servers": []any{
map[string]any{
"address": "127.0.0.1",
"port": 1080,
},
},
},
}
rawOutbound, _ := json.Marshal(outbound)
newOutbounds := []json_util.RawMessage{rawOutbound}
newOutbounds = append(newOutbounds, s.defaultOutbounds...)
newConfigJson := make(map[string]any)
maps.Copy(newConfigJson, s.configJson)
newConfigJson["outbounds"] = newOutbounds
newConfigJson["remarks"] = remark
newConfig, _ := json.MarshalIndent(newConfigJson, "", " ")
return newConfig
}
func mergeFinalMask(base any, extra map[string]any) map[string]any {
merged := map[string]any{}
if baseMap, ok := base.(map[string]any); ok {