diff --git a/internal/automation/prefix.go b/internal/automation/prefix.go index d2c0ada..2a2d373 100644 --- a/internal/automation/prefix.go +++ b/internal/automation/prefix.go @@ -25,12 +25,22 @@ func PrefixProxyNames(subscriptionName string, data map[string]interface{}) []Pr refs := renameProxies(subscriptionName, data, rename) renameProxyGroups(subscriptionName, data, rename) - rewriteGroupProxyReferences(data, rename) - rewriteRuleTargets(data, rename) + RewriteReferences(data, rename) return refs } +// RewriteReferences rewrites every reference to a renamed proxy or +// proxy-group — inside other groups' "proxies" lists and inside +// data["rules"]' rule targets — to the new name. Exposed separately from +// PrefixProxyNames so callers that rename groups after the fact (e.g. +// collapsing several subscriptions' default selector group into one) can +// reuse the same rewrite logic without re-running the proxy/group renaming. +func RewriteReferences(data map[string]interface{}, rename map[string]string) { + rewriteGroupProxyReferences(data, rename) + rewriteRuleTargets(data, rename) +} + func renameProxies(subscriptionName string, data map[string]interface{}, rename map[string]string) []ProxyRef { proxiesRaw, ok := data["proxies"].([]interface{}) if !ok { diff --git a/internal/corecfg/manager.go b/internal/corecfg/manager.go index 8b42ebd..d289611 100644 --- a/internal/corecfg/manager.go +++ b/internal/corecfg/manager.go @@ -350,6 +350,7 @@ func (m *Manager) resolveSubscriptions() []*model.Subscription { func (m *Manager) mergeSubscriptions(subs []*model.Subscription) (map[string]interface{}, []automation.ProxyRef, bool) { combined := map[string]interface{}{} var proxies []automation.ProxyRef + var selectionGroups []map[string]interface{} for _, sub := range subs { filePath := sub.GetFilePath(m.Storage.ConfigDir) @@ -374,10 +375,14 @@ func (m *Manager) mergeSubscriptions(subs []*model.Subscription) (map[string]int } proxies = append(proxies, automation.PrefixProxyNames(sub.Name, data)...) + if group := popFirstProxyGroup(data); group != nil { + selectionGroups = append(selectionGroups, group) + } combined = deepMerge(combined, data) } combined["rules"] = dedupeMatchRule(combined["rules"]) + mergeProxySelection(combined, selectionGroups) return combined, proxies, true } diff --git a/internal/corecfg/merge.go b/internal/corecfg/merge.go index 5d2af77..69d7476 100644 --- a/internal/corecfg/merge.go +++ b/internal/corecfg/merge.go @@ -1,5 +1,7 @@ package corecfg +import "gitea.epss.net.cn/klesh/ss/internal/automation" + // deepMerge merges dict2 into dict1 in place and returns dict1, mirroring // corecfg_manager.py's module-level deep_merge(): nested maps recurse, // matching lists are concatenated, everything else is overwritten by dict2. @@ -25,3 +27,74 @@ func deepMerge(dict1, dict2 map[string]interface{}) map[string]interface{} { } return dict1 } + +// popFirstProxyGroup removes and returns data["proxy-groups"]' first entry — +// by convention a subscription's default selector group, the one its own +// rules point at — so mergeProxySelection can fold every subscription's +// default selector into one combined group. Returns nil if there is none. +func popFirstProxyGroup(data map[string]interface{}) map[string]interface{} { + groupsRaw, ok := data["proxy-groups"].([]interface{}) + if !ok || len(groupsRaw) == 0 { + return nil + } + + group, ok := groupsRaw[0].(map[string]interface{}) + if !ok { + return nil + } + + data["proxy-groups"] = groupsRaw[1:] + return group +} + +// selectionGroupName is the name given to the proxy-group produced by +// folding every merged subscription's default selector group together. +const selectionGroupName = "Proxy Selection" + +// mergeProxySelection collapses each subscription's default selector group +// (already popped off by popFirstProxyGroup) into a single combined group +// named selectionGroupName, covering every subscription's proxies, and +// rewrites every rule/group reference to the old per-subscription groups so +// they point at it instead. +func mergeProxySelection(combined map[string]interface{}, groups []map[string]interface{}) { + if len(groups) == 0 { + return + } + + rename := make(map[string]string, len(groups)) + seen := make(map[string]bool) + var groupType interface{} + var mergedProxies []interface{} + + for _, group := range groups { + if name, ok := group["name"].(string); ok { + rename[name] = selectionGroupName + } + if groupType == nil { + groupType = group["type"] + } + if list, ok := group["proxies"].([]interface{}); ok { + for _, p := range list { + name, ok := p.(string) + if ok && seen[name] { + continue + } + if ok { + seen[name] = true + } + mergedProxies = append(mergedProxies, p) + } + } + } + + merged := map[string]interface{}{ + "name": selectionGroupName, + "type": groupType, + "proxies": mergedProxies, + } + + existing, _ := combined["proxy-groups"].([]interface{}) + combined["proxy-groups"] = append([]interface{}{merged}, existing...) + + automation.RewriteReferences(combined, rename) +} diff --git a/internal/corecfg/merge_test.go b/internal/corecfg/merge_test.go new file mode 100644 index 0000000..ccbd236 --- /dev/null +++ b/internal/corecfg/merge_test.go @@ -0,0 +1,82 @@ +package corecfg + +import ( + "reflect" + "testing" +) + +func TestPopFirstProxyGroup(t *testing.T) { + data := map[string]interface{}{ + "proxy-groups": []interface{}{ + map[string]interface{}{"name": "Proxy", "type": "select", "proxies": []interface{}{"Auto"}}, + map[string]interface{}{"name": "Auto", "type": "url-test", "proxies": []interface{}{"HK"}}, + }, + } + + got := popFirstProxyGroup(data) + + want := map[string]interface{}{"name": "Proxy", "type": "select", "proxies": []interface{}{"Auto"}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %#v, want %#v", got, want) + } + + remaining := data["proxy-groups"].([]interface{}) + if len(remaining) != 1 || remaining[0].(map[string]interface{})["name"] != "Auto" { + t.Fatalf("expected only the Auto group to remain, got %#v", remaining) + } +} + +func TestPopFirstProxyGroupEmpty(t *testing.T) { + if got := popFirstProxyGroup(map[string]interface{}{}); got != nil { + t.Fatalf("expected nil, got %#v", got) + } +} + +func TestMergeProxySelection(t *testing.T) { + combined := map[string]interface{}{ + "proxy-groups": []interface{}{ + map[string]interface{}{"name": "sub-a | Auto", "type": "url-test", "proxies": []interface{}{"sub-a | HK"}}, + }, + "rules": []interface{}{ + "DOMAIN,example.com,sub-a | Proxy", + "DOMAIN,other.com,sub-b | Proxy", + }, + } + groups := []map[string]interface{}{ + {"name": "sub-a | Proxy", "type": "select", "proxies": []interface{}{"sub-a | Auto", "DIRECT"}}, + {"name": "sub-b | Proxy", "type": "select", "proxies": []interface{}{"sub-b | Auto", "DIRECT"}}, + } + + mergeProxySelection(combined, groups) + + proxyGroups := combined["proxy-groups"].([]interface{}) + if len(proxyGroups) != 2 { + t.Fatalf("expected 2 proxy-groups (merged selection + sub-a | Auto), got %#v", proxyGroups) + } + + merged := proxyGroups[0].(map[string]interface{}) + if merged["name"] != selectionGroupName { + t.Fatalf("expected first group to be %q, got %#v", selectionGroupName, merged["name"]) + } + wantProxies := []interface{}{"sub-a | Auto", "DIRECT", "sub-b | Auto"} + if !reflect.DeepEqual(merged["proxies"], wantProxies) { + t.Fatalf("got proxies %#v, want %#v", merged["proxies"], wantProxies) + } + + wantRules := []interface{}{ + "DOMAIN,example.com," + selectionGroupName, + "DOMAIN,other.com," + selectionGroupName, + } + if !reflect.DeepEqual(combined["rules"], wantRules) { + t.Fatalf("got rules %#v, want %#v", combined["rules"], wantRules) + } +} + +func TestMergeProxySelectionNoGroups(t *testing.T) { + combined := map[string]interface{}{"proxy-groups": []interface{}{"unchanged"}} + mergeProxySelection(combined, nil) + + if !reflect.DeepEqual(combined["proxy-groups"], []interface{}{"unchanged"}) { + t.Fatalf("expected combined to be untouched, got %#v", combined) + } +}