fix: fetcher proxy support, Dockerfile sing-box, rename index format
1. sslinks 403: Cloudflare blocks server IP. Add fetcher.proxy_url config so subscription/flow requests can route through an HTTP proxy. 2. Egress probe: Docker image lacked sing-box. Install sing-box v1.11.4 in the Alpine runtime stage. 3. Collection rename: group by display baseName instead of internal groupKey, format index as -N instead of %02d. Fixes double-suffix issue where EnsureUniqueProxyNames appended -2/-3 onto existing 02.
This commit is contained in:
@@ -30,14 +30,17 @@ func resolveRenameOptions(opts *model.RenameOptions) model.RenameOptions {
|
||||
|
||||
// RenameCollectionNodes renames proxy nodes in the collection-level format:
|
||||
//
|
||||
// [emoji country flag] [alias] [country name] [city] [01...100]
|
||||
// [emoji country flag] [alias] [country name] [city] [-1...-N]
|
||||
//
|
||||
// The alias is read from each node's "_sourceAlias" field (tagged by the
|
||||
// subscription service from the source's Alias). Geographic info (flag,
|
||||
// country, city) is auto-detected from the node's original name and server.
|
||||
// Nodes are grouped by country+city+alias and numbered sequentially within
|
||||
// each group, starting at 01. If only one node exists in a group, no number
|
||||
// is appended.
|
||||
//
|
||||
// Nodes are grouped by their **display baseName** (the name without the index
|
||||
// suffix) and numbered sequentially within each group, starting at 1. This
|
||||
// ensures that nodes producing the same display name get unique suffixes
|
||||
// (-1, -2, -3, …) in a single pass, so EnsureUniqueProxyNames never needs to
|
||||
// append a secondary suffix.
|
||||
//
|
||||
// opts controls which fields are included in the output. A nil opts means all
|
||||
// fields are included (full default format).
|
||||
@@ -48,19 +51,16 @@ func RenameCollectionNodes(proxies []model.ProxyNode, opts *model.RenameOptions)
|
||||
|
||||
o := resolveRenameOptions(opts)
|
||||
|
||||
type groupKey struct {
|
||||
alias string
|
||||
country string
|
||||
city string
|
||||
// First pass: compute display baseName (without index) for each node.
|
||||
type nodeInfo struct {
|
||||
proxy model.ProxyNode
|
||||
baseName string // display name without index suffix; "" means skip rename
|
||||
}
|
||||
|
||||
groups := make(map[groupKey]int)
|
||||
results := make([]model.ProxyNode, len(proxies))
|
||||
|
||||
// First pass: count group sizes
|
||||
infos := make([]nodeInfo, len(proxies))
|
||||
for i, proxy := range proxies {
|
||||
if proxy == nil {
|
||||
results[i] = proxy
|
||||
infos[i] = nodeInfo{proxy: proxy}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -70,45 +70,10 @@ func RenameCollectionNodes(proxies []model.ProxyNode, opts *model.RenameOptions)
|
||||
|
||||
geo := util.DetectGeoWithServer(name, server)
|
||||
if geo.CountryName == "" {
|
||||
results[i] = proxy
|
||||
infos[i] = nodeInfo{proxy: proxy}
|
||||
continue
|
||||
}
|
||||
|
||||
key := groupKey{
|
||||
alias: alias,
|
||||
country: geo.CountryCN,
|
||||
city: geo.CityCN,
|
||||
}
|
||||
groups[key]++
|
||||
}
|
||||
|
||||
// Second pass: assign names with numbering
|
||||
counters := make(map[groupKey]int)
|
||||
for i, proxy := range proxies {
|
||||
if proxy == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name := ToString(proxy["name"])
|
||||
alias := ToString(proxy["_sourceAlias"])
|
||||
server := ToString(proxy["server"])
|
||||
|
||||
geo := util.DetectGeoWithServer(name, server)
|
||||
if geo.CountryName == "" {
|
||||
results[i] = proxy
|
||||
continue
|
||||
}
|
||||
|
||||
key := groupKey{
|
||||
alias: alias,
|
||||
country: geo.CountryCN,
|
||||
city: geo.CityCN,
|
||||
}
|
||||
|
||||
counters[key]++
|
||||
total := groups[key]
|
||||
|
||||
// Build name parts based on enabled options
|
||||
var parts []string
|
||||
|
||||
if o.Flag && geo.Flag != "" {
|
||||
@@ -131,15 +96,45 @@ func RenameCollectionNodes(proxies []model.ProxyNode, opts *model.RenameOptions)
|
||||
parts = append(parts, geo.CityCN)
|
||||
}
|
||||
|
||||
baseName := strings.Join(parts, " ")
|
||||
infos[i] = nodeInfo{
|
||||
proxy: proxy,
|
||||
baseName: strings.Join(parts, " "),
|
||||
}
|
||||
}
|
||||
|
||||
if o.Index && total > 1 {
|
||||
baseName = fmt.Sprintf("%s %02d", baseName, counters[key])
|
||||
// Count group sizes by baseName.
|
||||
baseCounts := make(map[string]int)
|
||||
for _, info := range infos {
|
||||
if info.baseName != "" {
|
||||
baseCounts[info.baseName]++
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: assign names with sequential numbering.
|
||||
counters := make(map[string]int)
|
||||
results := make([]model.ProxyNode, len(proxies))
|
||||
for i, 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
|
||||
}
|
||||
|
||||
next := cloneProxy(proxy)
|
||||
counters[info.baseName]++
|
||||
total := baseCounts[info.baseName]
|
||||
|
||||
name := info.baseName
|
||||
if o.Index && total > 1 {
|
||||
name = fmt.Sprintf("%s-%d", name, counters[info.baseName])
|
||||
}
|
||||
|
||||
next := cloneProxy(info.proxy)
|
||||
delete(next, "_sourceAlias")
|
||||
next["name"] = baseName
|
||||
next["name"] = name
|
||||
results[i] = next
|
||||
}
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ func TestRenameCollectionNodes_MultipleNodes(t *testing.T) {
|
||||
if len(result) != 3 {
|
||||
t.Fatalf("expected 3 nodes, got %d", len(result))
|
||||
}
|
||||
// All 3 should be numbered 01, 02, 03
|
||||
// All 3 should be numbered -1, -2, -3
|
||||
for i, node := range result {
|
||||
name := node["name"].(string)
|
||||
expected := fmt.Sprintf("%02d", i+1)
|
||||
expected := fmt.Sprintf("-%d", i+1)
|
||||
if !strings.Contains(name, expected) {
|
||||
t.Errorf("node[%d]: expected number %s in name %q", i, expected, name)
|
||||
}
|
||||
@@ -222,7 +222,7 @@ func TestRenameCollectionNodes_DisableIndex(t *testing.T) {
|
||||
for i, node := range result {
|
||||
name := node["name"].(string)
|
||||
// Should not contain number suffix
|
||||
suffix := fmt.Sprintf("%02d", i+1)
|
||||
suffix := fmt.Sprintf("-%d", i+1)
|
||||
if strings.HasSuffix(name, suffix) {
|
||||
t.Errorf("node[%d]: index should be disabled, got %q", i, name)
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func TestRenameCollectionNodes_OnlyFlagAndIndex(t *testing.T) {
|
||||
if strings.Contains(name, "[A]") {
|
||||
t.Errorf("node[%d]: alias should not appear, got %q", i, name)
|
||||
}
|
||||
expected := fmt.Sprintf("🇭🇰 %02d", i+1)
|
||||
expected := fmt.Sprintf("🇭🇰-%d", i+1)
|
||||
if name != expected {
|
||||
t.Errorf("node[%d]: expected %q, got %q", i, expected, name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user