feat: dedupe generic rules

This commit is contained in:
2026-07-20 17:42:48 +08:00
parent 8c574bf054
commit 69e7d05c10
2 changed files with 82 additions and 11 deletions

View File

@ -381,7 +381,7 @@ func (m *Manager) mergeSubscriptions(subs []*model.Subscription) (map[string]int
combined = deepMerge(combined, data)
}
combined["rules"] = dedupeMatchRule(dedupeGeoIPRule(combined["rules"]))
combined["rules"] = dedupeMatchRule(dedupeGeoIPRule(dedupeGenericRule(combined["rules"])))
mergeProxySelection(combined, selectionGroups)
return combined, proxies, true
@ -421,14 +421,14 @@ func dedupeGeoIPRule(rulesRaw interface{}) interface{} {
lastIndex := make(map[string]int)
for i, item := range rules {
if key, ok := geoIPRuleKey(item); ok {
if ruleType, key, ok := ruleKey(item); ok && ruleType == "GEOIP" {
lastIndex[key] = i
}
}
filtered := make([]interface{}, 0, len(rules))
for i, item := range rules {
if key, ok := geoIPRuleKey(item); ok && lastIndex[key] != i {
if ruleType, key, ok := ruleKey(item); ok && ruleType == "GEOIP" && lastIndex[key] != i {
continue
}
filtered = append(filtered, item)
@ -436,13 +436,50 @@ func dedupeGeoIPRule(rulesRaw interface{}) interface{} {
return filtered
}
// geoIPRuleKey returns the "GEOIP,<payload>" portion of a rule line (i.e.
// everything but its target and any trailing "no-resolve" modifier), and
// whether the line is a GEOIP rule at all.
func geoIPRuleKey(item interface{}) (string, bool) {
// dedupeGenericRule keeps only the first occurrence of each
// "TYPE,PAYLOAD" rule (e.g. "DOMAIN,example.com,...",
// "DOMAIN-SUFFIX,google.com,...", "IP-CIDR,10.0.0.0/8,...") contributed by
// the merged subscriptions, dropping the rest: Clash evaluates rules
// top-to-bottom, so once an earlier rule matches a given payload, any later
// duplicate for that same payload could never fire anyway. MATCH and GEOIP
// rules have their own dedup semantics (dedupeMatchRule, dedupeGeoIPRule),
// so they pass through here untouched.
func dedupeGenericRule(rulesRaw interface{}) interface{} {
rules, ok := rulesRaw.([]interface{})
if !ok {
return rulesRaw
}
seen := make(map[string]bool)
filtered := make([]interface{}, 0, len(rules))
for _, item := range rules {
ruleType, key, ok := ruleKey(item)
if !ok || ruleType == "MATCH" || ruleType == "GEOIP" {
filtered = append(filtered, item)
continue
}
if seen[key] {
continue
}
seen[key] = true
filtered = append(filtered, item)
}
return filtered
}
// ruleKey returns a rule line's type (e.g. "DOMAIN", "GEOIP", "MATCH") and
// its "TYPE,PAYLOAD" portion — everything but the target and any trailing
// "no-resolve" modifier — plus whether the line parses as a well-formed
// "TYPE,PAYLOAD,TARGET" (or "TYPE,TARGET" for MATCH) rule at all.
func ruleKey(item interface{}) (ruleType, key string, ok bool) {
line, ok := item.(string)
if !ok || !strings.HasPrefix(line, "GEOIP,") {
return "", false
if !ok {
return "", "", false
}
typeEnd := strings.Index(line, ",")
if typeEnd == -1 {
return "", "", false
}
body := line
@ -452,9 +489,10 @@ func geoIPRuleKey(item interface{}) (string, bool) {
idx := strings.LastIndex(body, ",")
if idx == -1 {
return line, true
return "", "", false
}
return body[:idx], true
return line[:typeEnd], body[:idx], true
}
func openInEditor(path string) bool {

View File

@ -58,3 +58,36 @@ func TestDedupeGeoIPRuleNonList(t *testing.T) {
t.Fatalf("expected nil passthrough, got %#v", got)
}
}
func TestDedupeGenericRule(t *testing.T) {
rules := []interface{}{
"DOMAIN,example.com,sub-a | Proxy",
"DOMAIN-SUFFIX,google.com,sub-a | Proxy",
"DOMAIN,example.com,sub-b | Proxy",
"IP-CIDR,10.0.0.0/8,DIRECT,no-resolve",
"DOMAIN-SUFFIX,google.com,sub-b | Proxy",
"IP-CIDR,10.0.0.0/8,sub-b | Proxy",
"GEOIP,CN,DIRECT",
"MATCH,sub-a | Auto",
}
got := dedupeGenericRule(rules)
want := []interface{}{
"DOMAIN,example.com,sub-a | Proxy",
"DOMAIN-SUFFIX,google.com,sub-a | Proxy",
"IP-CIDR,10.0.0.0/8,DIRECT,no-resolve",
"GEOIP,CN,DIRECT",
"MATCH,sub-a | Auto",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
}
func TestDedupeGenericRuleNonList(t *testing.T) {
if got := dedupeGenericRule(nil); got != nil {
t.Fatalf("expected nil passthrough, got %#v", got)
}
}