feat: support merging multiple subscriptions

This commit is contained in:
2026-07-19 23:58:02 +08:00
parent fb4d927220
commit 087869d84f
17 changed files with 1341 additions and 60 deletions

View File

@ -0,0 +1,151 @@
package automation
import (
"fmt"
"strings"
)
// PrefixProxyNames rewrites every proxy's "name" field in data["proxies"]
// and every proxy-group's "name" field in data["proxy-groups"] — both are
// scoped per-subscription and would otherwise collide across subscriptions
// the same way proxy names do — prefixing them with subscriptionName. It
// also rewrites every reference to a renamed proxy or proxy-group inside
// each group's own "proxies" list and inside data["rules"]' rule targets
// (e.g. "DOMAIN,example.com,Proxy" or "MATCH,Auto"), since those are all
// resolved by name and would otherwise silently point at nothing after the
// rename. References to anything else (DIRECT, REJECT, a proxy-provider
// name, ...) are left untouched.
//
// Returns a ProxyRef per proxy, for later ssm matching/grouping.
func PrefixProxyNames(subscriptionName string, data map[string]interface{}) []ProxyRef {
// Original name (of either a proxy or a proxy-group) -> prefixed
// display name. Clash requires proxy and group names to share a single
// namespace within one config, so a single map is correct here too.
rename := make(map[string]string)
refs := renameProxies(subscriptionName, data, rename)
renameProxyGroups(subscriptionName, data, rename)
rewriteGroupProxyReferences(data, rename)
rewriteRuleTargets(data, rename)
return refs
}
func renameProxies(subscriptionName string, data map[string]interface{}, rename map[string]string) []ProxyRef {
proxiesRaw, ok := data["proxies"].([]interface{})
if !ok {
return nil
}
refs := make([]ProxyRef, 0, len(proxiesRaw))
for _, item := range proxiesRaw {
proxy, ok := item.(map[string]interface{})
if !ok {
continue
}
name, ok := proxy["name"].(string)
if !ok {
continue
}
display := fmt.Sprintf("%s | %s", subscriptionName, name)
proxy["name"] = display
rename[name] = display
refs = append(refs, ProxyRef{
OriginalName: name,
DisplayName: display,
Subscription: subscriptionName,
})
}
return refs
}
func renameProxyGroups(subscriptionName string, data map[string]interface{}, rename map[string]string) {
for _, group := range proxyGroupMaps(data) {
name, ok := group["name"].(string)
if !ok {
continue
}
display := fmt.Sprintf("%s | %s", subscriptionName, name)
group["name"] = display
rename[name] = display
}
}
func rewriteGroupProxyReferences(data map[string]interface{}, rename map[string]string) {
for _, group := range proxyGroupMaps(data) {
proxiesRaw, ok := group["proxies"].([]interface{})
if !ok {
continue
}
for i, p := range proxiesRaw {
name, ok := p.(string)
if !ok {
continue
}
if display, renamed := rename[name]; renamed {
proxiesRaw[i] = display
}
}
}
}
// rewriteRuleTargets rewrites the target field of every rule in
// data["rules"] that references a renamed proxy or proxy-group. Clash rule
// lines are a comma-separated "TYPE,payload,TARGET[,no-resolve]" (or just
// "MATCH,TARGET" with no payload) — the target is always the last field,
// except for the handful of rule types that allow a trailing "no-resolve"
// modifier, where it's the second-to-last. Lines with no comma at all are
// left untouched (not valid rule syntax to begin with).
func rewriteRuleTargets(data map[string]interface{}, rename map[string]string) {
rulesRaw, ok := data["rules"].([]interface{})
if !ok {
return
}
for i, item := range rulesRaw {
line, ok := item.(string)
if !ok {
continue
}
rulesRaw[i] = rewriteRuleLine(line, rename)
}
}
func rewriteRuleLine(line string, rename map[string]string) string {
body := line
suffix := ""
if idx := strings.LastIndex(body, ","); idx != -1 && strings.EqualFold(body[idx+1:], "no-resolve") {
suffix = "," + body[idx+1:]
body = body[:idx]
}
idx := strings.LastIndex(body, ",")
if idx == -1 {
return line
}
target := body[idx+1:]
if display, ok := rename[target]; ok {
body = body[:idx+1] + display
}
return body + suffix
}
// proxyGroupMaps returns data["proxy-groups"]'s entries as maps, skipping
// anything malformed.
func proxyGroupMaps(data map[string]interface{}) []map[string]interface{} {
groupsRaw, ok := data["proxy-groups"].([]interface{})
if !ok {
return nil
}
groups := make([]map[string]interface{}, 0, len(groupsRaw))
for _, item := range groupsRaw {
if group, ok := item.(map[string]interface{}); ok {
groups = append(groups, group)
}
}
return groups
}