瀏覽代碼

fix(sub): ensure unique Clash proxy names (#4641)

genRemark can return an empty string (remark-less inbound, or a remark model that depends on the email the Clash path drops), which was set verbatim as the proxy name. mihomo rejects the whole config on a duplicate name, so two such proxies made the Clash Verge profile vanish on refresh; a single one was dropped from the PROXY group, collapsing it to DIRECT so Rule mode stopped proxying while Global still worked. Guarantee every proxy carries a non-empty, unique name before assembling the group.
MHSanaei 10 小時之前
父節點
當前提交
d29a17d333
共有 2 個文件被更改,包括 68 次插入0 次删除
  1. 34 0
      sub/subClashService.go
  2. 34 0
      sub/subClashService_test.go

+ 34 - 0
sub/subClashService.go

@@ -60,6 +60,8 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
 		return "", "", nil
 	}
 
+	ensureUniqueProxyNames(proxies)
+
 	emails := make([]string, 0, len(seenEmails))
 	for e := range seenEmails {
 		emails = append(emails, e)
@@ -93,6 +95,38 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
 	return string(finalYAML), header, nil
 }
 
+// ensureUniqueProxyNames keeps every proxy "name" non-empty and unique:
+// mihomo rejects the whole config on a duplicate name (the empty string
+// genRemark returns for a remark-less inbound counts), vanishing the Clash
+// profile on refresh. See issue #4641.
+func ensureUniqueProxyNames(proxies []map[string]any) {
+	seen := make(map[string]struct{}, len(proxies))
+	for i, proxy := range proxies {
+		base, _ := proxy["name"].(string)
+		if base == "" {
+			base = fallbackProxyName(proxy, i)
+		}
+		name := base
+		for n := 2; ; n++ {
+			if _, dup := seen[name]; !dup {
+				break
+			}
+			name = fmt.Sprintf("%s-%d", base, n)
+		}
+		seen[name] = struct{}{}
+		proxy["name"] = name
+	}
+}
+
+func fallbackProxyName(proxy map[string]any, idx int) string {
+	typ, _ := proxy["type"].(string)
+	server, _ := proxy["server"].(string)
+	if typ != "" && server != "" {
+		return fmt.Sprintf("%s-%s-%v", typ, server, proxy["port"])
+	}
+	return fmt.Sprintf("proxy-%d", idx+1)
+}
+
 func (s *SubClashService) getProxies(inbound *model.Inbound, client model.Client, host string) []map[string]any {
 	stream := s.streamData(inbound.StreamSettings)
 	// For node-managed inbounds the Clash proxy "server" must be the

+ 34 - 0
sub/subClashService_test.go

@@ -5,6 +5,40 @@ import (
 	"testing"
 )
 
+func TestEnsureUniqueProxyNames(t *testing.T) {
+	proxies := []map[string]any{
+		{"name": "", "type": "vless", "server": "a.com", "port": 443},
+		{"name": "", "type": "vmess", "server": "b.com", "port": 8443},
+		{"name": "node"},
+		{"name": "node"},
+		{"name": ""},
+	}
+
+	ensureUniqueProxyNames(proxies)
+
+	seen := map[string]bool{}
+	for i, p := range proxies {
+		name, _ := p["name"].(string)
+		if name == "" {
+			t.Fatalf("proxy %d still has an empty name (mihomo would reject the config, #4641)", i)
+		}
+		if seen[name] {
+			t.Fatalf("proxy %d has duplicate name %q (mihomo rejects the whole config, #4641)", i, name)
+		}
+		seen[name] = true
+	}
+
+	if got := proxies[0]["name"]; got != "vless-a.com-443" {
+		t.Errorf("empty name fallback = %q, want vless-a.com-443", got)
+	}
+	if proxies[2]["name"] == proxies[3]["name"] {
+		t.Errorf("duplicate %q was not disambiguated", proxies[2]["name"])
+	}
+	if got := proxies[4]["name"]; got != "proxy-5" {
+		t.Errorf("typeless empty name fallback = %q, want proxy-5", got)
+	}
+}
+
 func TestApplyTransport_XHTTP(t *testing.T) {
 	svc := &SubClashService{}
 	proxy := map[string]any{}