fix: drop nodes that fail geo detection in collection rename

Nodes whose country cannot be detected are now filtered out instead of
kept with their original name. This prevents unlabelled/unrenamed nodes
from appearing in collection output.
This commit is contained in:
2026-07-29 11:19:19 +08:00
parent 71f814d1ce
commit 585164dca6
2 changed files with 18 additions and 34 deletions
+16 -28
View File
@@ -30,17 +30,18 @@ func resolveRenameOptions(opts *model.RenameOptions) model.RenameOptions {
// RenameCollectionNodes renames proxy nodes in the collection-level format: // RenameCollectionNodes renames proxy nodes in the collection-level format:
// //
// [emoji country flag] [alias] [country name] [city] [-1...-N] // [emoji country flag] [alias] [country name] [city] [01...NN]
// //
// The alias is read from each node's "_sourceAlias" field (tagged by the // The alias is read from each node's "_sourceAlias" field (tagged by the
// subscription service from the source's Alias). Geographic info (flag, // subscription service from the source's Alias). Geographic info (flag,
// country, city) is auto-detected from the node's original name and server. // country, city) is auto-detected from the node's original name and server.
// //
// Nodes are grouped by their **display baseName** (the name without the index // Nodes are grouped by their **display baseName** (the name without the index
// suffix) and numbered sequentially within each group, starting at 1. This // suffix) and numbered sequentially within each group, starting at 1.
// ensures that nodes producing the same display name get unique suffixes //
// (-1, -2, -3, …) in a single pass, so EnsureUniqueProxyNames never needs to // Nodes whose geographic info cannot be detected (no country found) are
// append a secondary suffix. // dropped from the output — they cannot be meaningfully renamed and would
// appear as unlabelled entries in the collection.
// //
// opts controls which fields are included in the output. A nil opts means all // opts controls which fields are included in the output. A nil opts means all
// fields are included (full default format). // fields are included (full default format).
@@ -54,13 +55,12 @@ func RenameCollectionNodes(proxies []model.ProxyNode, opts *model.RenameOptions)
// First pass: compute display baseName (without index) for each node. // First pass: compute display baseName (without index) for each node.
type nodeInfo struct { type nodeInfo struct {
proxy model.ProxyNode proxy model.ProxyNode
baseName string // display name without index suffix; "" means skip rename baseName string // display name without index suffix; "" means geo failed
} }
infos := make([]nodeInfo, len(proxies)) infos := make([]nodeInfo, 0, len(proxies))
for i, proxy := range proxies { for _, proxy := range proxies {
if proxy == nil { if proxy == nil {
infos[i] = nodeInfo{proxy: proxy}
continue continue
} }
@@ -70,7 +70,7 @@ func RenameCollectionNodes(proxies []model.ProxyNode, opts *model.RenameOptions)
geo := util.DetectGeoWithServer(name, server) geo := util.DetectGeoWithServer(name, server)
if geo.CountryName == "" { if geo.CountryName == "" {
infos[i] = nodeInfo{proxy: proxy} // Geo detection failed — skip this node
continue continue
} }
@@ -96,34 +96,22 @@ func RenameCollectionNodes(proxies []model.ProxyNode, opts *model.RenameOptions)
parts = append(parts, geo.CityCN) parts = append(parts, geo.CityCN)
} }
infos[i] = nodeInfo{ infos = append(infos, nodeInfo{
proxy: proxy, proxy: proxy,
baseName: strings.Join(parts, " "), baseName: strings.Join(parts, " "),
} })
} }
// Count group sizes by baseName. // Count group sizes by baseName.
baseCounts := make(map[string]int) baseCounts := make(map[string]int)
for _, info := range infos { for _, info := range infos {
if info.baseName != "" { baseCounts[info.baseName]++
baseCounts[info.baseName]++
}
} }
// Second pass: assign names with sequential numbering. // Second pass: assign names with sequential numbering.
counters := make(map[string]int) counters := make(map[string]int)
results := make([]model.ProxyNode, len(proxies)) results := make([]model.ProxyNode, 0, len(infos))
for i, info := range infos { for _, info := range infos {
if info.proxy == nil {
results[i] = info.proxy
continue
}
if info.baseName == "" {
// Geo detection failed — keep original name.
results[i] = info.proxy
continue
}
counters[info.baseName]++ counters[info.baseName]++
total := baseCounts[info.baseName] total := baseCounts[info.baseName]
@@ -135,7 +123,7 @@ func RenameCollectionNodes(proxies []model.ProxyNode, opts *model.RenameOptions)
next := cloneProxy(info.proxy) next := cloneProxy(info.proxy)
delete(next, "_sourceAlias") delete(next, "_sourceAlias")
next["name"] = name next["name"] = name
results[i] = next results = append(results, next)
} }
return results return results
+2 -6
View File
@@ -110,12 +110,8 @@ func TestRenameCollectionNodes_UnknownCountry(t *testing.T) {
{"name": "Unknown Node", "type": "ss", "_sourceAlias": "A"}, {"name": "Unknown Node", "type": "ss", "_sourceAlias": "A"},
} }
result := RenameCollectionNodes(proxies, nil) result := RenameCollectionNodes(proxies, nil)
if len(result) != 1 { if len(result) != 0 {
t.Fatalf("expected 1 node, got %d", len(result)) t.Fatalf("expected 0 nodes (geo detection failed, should be filtered), got %d", len(result))
}
// Should keep original name
if result[0]["name"] != "Unknown Node" {
t.Errorf("expected original name, got %q", result[0]["name"])
} }
} }