feat: evolve rate-filter to a more generic filter

This commit is contained in:
2026-07-28 20:40:12 +08:00
parent a3e3079af1
commit b206594eb8
15 changed files with 492 additions and 193 deletions

View File

@ -0,0 +1,133 @@
package automation
import (
"fmt"
"regexp"
"strconv"
)
// applyFilters implements the "filters" ssm feature: for each rule, it
// scopes down to the proxies matched by rule.Match, decides which of those
// are unwanted, and strips them out of every proxy-group's `proxies` list
// in finalConfig — both groups merged in from subscriptions and ones built
// by a proxy-groups rule. A scoped proxy is unwanted if rule.Compares is
// empty (Match alone acts as a denylist), or if it satisfies every entry in
// Compares (AND'd) when Compares is non-empty; a proxy whose name doesn't
// match a Compares entry's Pattern at all can't be evaluated and is left
// alone. Returns one error per rule with an invalid pattern/operator/match;
// other rules still run.
func applyFilters(finalConfig map[string]interface{}, rules []FilterRule, proxies []ProxyRef) []error {
var errs []error
for _, rule := range rules {
candidates, err := matchProxies(proxies, rule.Match)
if err != nil {
errs = append(errs, fmt.Errorf("filter '%s': %w", rule.Name, err))
continue
}
compares, err := compileCompares(rule.Compares)
if err != nil {
errs = append(errs, fmt.Errorf("filter '%s': %w", rule.Name, err))
continue
}
unwanted := make(map[string]bool, len(candidates))
for _, p := range candidates {
if len(compares) == 0 || satisfiesCompares(compares, p.OriginalName) {
unwanted[p.DisplayName] = true
}
}
if len(unwanted) == 0 {
fmt.Printf(" ssm: filter '%s' matched 0 proxies to remove\n", rule.Name)
continue
}
removeFromProxyGroups(finalConfig, unwanted)
}
return errs
}
// removeFromProxyGroups strips every proxy in unwanted out of each proxy
// group's `proxies` list, leaving group references, DIRECT/REJECT, etc.
// untouched.
func removeFromProxyGroups(finalConfig map[string]interface{}, unwanted map[string]bool) {
groups, _ := finalConfig["proxy-groups"].([]interface{})
for _, g := range groups {
group, ok := g.(map[string]interface{})
if !ok {
continue
}
list, ok := group["proxies"].([]interface{})
if !ok {
continue
}
kept := make([]interface{}, 0, len(list))
for _, item := range list {
if name, ok := item.(string); ok && unwanted[name] {
continue
}
kept = append(kept, item)
}
group["proxies"] = kept
}
}
type compiledCompare struct {
re *regexp.Regexp
cmp func(a, b float64) bool
value float64
}
func compileCompares(rules []CompareRule) ([]compiledCompare, error) {
compiled := make([]compiledCompare, 0, len(rules))
for _, c := range rules {
re, err := regexp.Compile(c.Pattern)
if err != nil {
return nil, fmt.Errorf("invalid pattern %q: %w", c.Pattern, err)
}
cmp, err := comparator(c.Operator)
if err != nil {
return nil, err
}
compiled = append(compiled, compiledCompare{re: re, cmp: cmp, value: c.Value})
}
return compiled, nil
}
func satisfiesCompares(compares []compiledCompare, name string) bool {
for _, c := range compares {
m := c.re.FindStringSubmatch(name)
if len(m) < 2 {
return false
}
extracted, err := strconv.ParseFloat(m[1], 64)
if err != nil {
return false
}
if !c.cmp(extracted, c.value) {
return false
}
}
return true
}
func comparator(op string) (func(a, b float64) bool, error) {
switch op {
case "<":
return func(a, b float64) bool { return a < b }, nil
case "<=":
return func(a, b float64) bool { return a <= b }, nil
case ">":
return func(a, b float64) bool { return a > b }, nil
case ">=":
return func(a, b float64) bool { return a >= b }, nil
case "==", "=":
return func(a, b float64) bool { return a == b }, nil
case "!=":
return func(a, b float64) bool { return a != b }, nil
default:
return nil, fmt.Errorf("unknown operator %q (expected <, <=, >, >=, ==, !=)", op)
}
}