package render import ( "regexp" "strings" "github.com/peterqiu0516/sub-store/internal/model" "github.com/peterqiu0516/sub-store/internal/util" "gopkg.in/yaml.v3" ) // RenderMihomoYaml renders proxies + template into a Mihomo/stash YAML document. func RenderMihomoYaml(proxies []model.ProxyNode, requestUrl string, template map[string]any) string { cfg := template if cfg == nil { cfg = map[string]any{} } mixedPort := firstInt(cfg, "mixedPort", "mixed-port") if mixedPort == 0 { mixedPort = 7890 } allowLan := firstBool(cfg, "allowLan", "allow-lan") // default false logLevel := firstStr(cfg, "logLevel", "log-level") if logLevel == "" { logLevel = "info" } mode := firstStr(cfg, "mode") if mode == "" { mode = "rule" } groupTemplates := extractGroupTemplates(cfg) if len(groupTemplates) == 0 { groupTemplates = DefaultProxyGroups() } ruleProviders := firstAny(cfg, "ruleProviders", "rule-providers") rules, _ := cfg["rules"].([]any) var rulesList []string for _, r := range rules { if s, ok := r.(string); ok && s != "" { rulesList = append(rulesList, s) } } if len(rulesList) == 0 { rulesList = []string{"MATCH,🚀 节ç‚č选择"} } // Build proxies list with undefined stripped var proxiesList []map[string]any for _, p := range proxies { proxiesList = append(proxiesList, util.StripUndefined(p)) } doc := map[string]any{ "mixed-port": mixedPort, "allow-lan": allowLan, "mode": mode, "log-level": logLevel, "proxies": proxiesList, "proxy-groups": RenderTemplateProxyGroups(proxies, groupTemplates), "rules": rulesList, } if dns := cfg["dns"]; dns != nil { doc["dns"] = dns } if sniffer := cfg["sniffer"]; sniffer != nil { doc["sniffer"] = sniffer } if ruleProviders != nil { doc["rule-providers"] = ruleProviders } var sb strings.Builder sb.WriteString("# Generated by Sub-Store\n") sb.WriteString("# Source: " + sourcePath(requestUrl) + "\n") data, err := yaml.Marshal(doc) if err != nil { sb.WriteString("# yaml marshal error: " + err.Error() + "\n") return sb.String() } sb.Write(data) return sb.String() } // DefaultProxyGroups returns the 3 default proxy groups per review-resolution #41. func DefaultProxyGroups() []map[string]any { return []map[string]any{ { "name": "🚀 节ç‚č选择", "type": "select", "proxies": []string{"♻ è‡ȘćŠšé€‰æ‹©", "🚀 æ‰‹ćŠšćˆ‡æą", "DIRECT"}, }, { "name": "♻ è‡ȘćŠšé€‰æ‹©", "type": "url-test", "proxies": []string{"$all"}, "url": model.TestURL, "interval": 300, "tolerance": 50, }, { "name": "🚀 æ‰‹ćŠšćˆ‡æą", "type": "select", "proxies": []string{"$all"}, }, } } // RenderTemplateProxyGroups expands group templates, filters by regex, removes // empty groups, and filters out references to non-existent nodes/groups. func RenderTemplateProxyGroups(proxies []model.ProxyNode, groupTemplates []map[string]any) []map[string]any { nodeNames := make([]string, 0, len(proxies)) for _, p := range proxies { nodeNames = append(nodeNames, getString(p, "name")) } // Expand proxies for each group type expandedGroup struct { template map[string]any proxies []string } var expanded []expandedGroup for _, group := range groupTemplates { expanded = append(expanded, expandedGroup{ template: group, proxies: ExpandGroupProxies(group, nodeNames), }) } // Collect names of groups that have at least one proxy entry includedGroupNames := make(map[string]bool) for _, g := range expanded { if len(g.proxies) > 0 { includedGroupNames[getString(g.template, "name")] = true } } allowedLiterals := map[string]bool{"DIRECT": true, "REJECT": true, "PASS": true} var result []map[string]any for _, g := range expanded { var proxyEntries []string seen := make(map[string]bool) for _, name := range g.proxies { if seen[name] { continue } seen[name] = true // keep if it's a node name, a valid group name, or an allowed literal if containsString(nodeNames, name) || includedGroupNames[name] || allowedLiterals[name] { proxyEntries = append(proxyEntries, name) } } if len(proxyEntries) == 0 { continue } out := map[string]any{} for k, v := range g.template { if k == "filter" || k == "proxies" { continue } if v == nil { continue } if s, ok := v.(string); ok && s == "" { continue } out[k] = v } out["proxies"] = proxyEntries result = append(result, out) } return result } // ExpandGroupProxies expands $all to all node names and applies the filter regex. func ExpandGroupProxies(group map[string]any, nodeNames []string) []string { var entries []string if filterStr, ok := group["filter"].(string); ok && filterStr != "" { entries = append(entries, findNamesByRegex(nodeNames, filterStr)...) } if rawProxies, ok := group["proxies"].([]any); ok { for _, item := range rawProxies { s, _ := item.(string) if s == "" { continue } if s == "$all" { entries = append(entries, nodeNames...) } else { entries = append(entries, s) } } } return uniqueStrings(entries) } func findNamesByRegex(names []string, pattern string) []string { re := compileRegex(pattern) if re == nil { return nil } var result []string for _, name := range names { if re.MatchString(name) { result = append(result, name) } } return result } func compileRegex(input string) *regexp.Regexp { pattern := input flags := "" if strings.HasPrefix(input, "(?i)") { pattern = input[4:] flags = "i" } re, err := regexp.Compile(flags + pattern) if err != nil { return nil } return re } // --- template config helpers --- func extractGroupTemplates(cfg map[string]any) []map[string]any { if v, ok := cfg["proxyGroups"]; ok { return toGroupList(v) } if v, ok := cfg["proxy-groups"]; ok { return toGroupList(v) } return nil } func toGroupList(v any) []map[string]any { arr, ok := v.([]any) if !ok { return nil } var result []map[string]any for _, item := range arr { if m, ok := item.(map[string]any); ok { result = append(result, m) } } return result } func firstStr(cfg map[string]any, keys ...string) string { for _, k := range keys { if v, ok := cfg[k]; ok { if s, ok := v.(string); ok && s != "" { return s } } } return "" } func firstInt(cfg map[string]any, keys ...string) int { for _, k := range keys { if v, ok := cfg[k]; ok { switch n := v.(type) { case int: return n case int64: return int(n) case float64: return int(n) } } } return 0 } func firstBool(cfg map[string]any, keys ...string) bool { for _, k := range keys { if v, ok := cfg[k]; ok { if b, ok := v.(bool); ok { return b } } } return false } func firstAny(cfg map[string]any, keys ...string) any { for _, k := range keys { if v, ok := cfg[k]; ok && v != nil { return v } } return nil } func containsString(list []string, s string) bool { for _, item := range list { if item == s { return true } } return false } func uniqueStrings(values []string) []string { seen := make(map[string]bool, len(values)) var result []string for _, v := range values { if v == "" { continue } if seen[v] { continue } seen[v] = true result = append(result, v) } return result }