package protocol import ( "fmt" "strings" "xboard-go/internal/model" "xboard-go/internal/service" "github.com/goccy/go-yaml" ) func GenerateClash(servers []model.Server, user model.User) (string, error) { return GenerateClashWithTemplate("clash", servers, user) } func GenerateClashWithTemplate(templateName string, servers []model.Server, user model.User) (string, error) { template := strings.TrimSpace(service.GetSubscribeTemplate(templateName)) if template == "" { return generateClashFallback(servers, user), nil } var config map[string]any if err := yaml.Unmarshal([]byte(template), &config); err != nil { return generateClashFallback(servers, user), nil } proxies := make([]any, 0, len(servers)) proxyNames := make([]string, 0, len(servers)) for _, s := range servers { conf := service.BuildNodeConfig(&s) proxy := buildClashProxy(conf, user) if proxy == nil { continue } proxies = append(proxies, proxy) proxyNames = append(proxyNames, conf.Name) } config["proxies"] = append(anySlice(config["proxies"]), proxies...) config["proxy-groups"] = mergeClashProxyGroups(anySlice(config["proxy-groups"]), proxyNames) if _, ok := config["rules"]; !ok { config["rules"] = []any{"MATCH,Proxy"} } data, err := yaml.Marshal(config) if err != nil { return "", err } output := strings.ReplaceAll(string(data), "$app_name", service.MustGetString("app_name", "XBoard")) return output, nil } func buildClashProxy(conf service.NodeServerConfig, user model.User) map[string]any { switch conf.Protocol { case "shadowsocks": cipher, _ := conf.Cipher.(string) return map[string]any{ "name": conf.Name, "type": "ss", "server": conf.RawHost, "port": conf.ServerPort, "cipher": cipher, "password": user.UUID, } case "vmess": return map[string]any{ "name": conf.Name, "type": "vmess", "server": conf.RawHost, "port": conf.ServerPort, "uuid": user.UUID, "alterId": 0, "cipher": "auto", "udp": true, } case "trojan": return map[string]any{ "name": conf.Name, "type": "trojan", "server": conf.RawHost, "port": conf.ServerPort, "password": user.UUID, "udp": true, } default: return nil } } func mergeClashProxyGroups(groups []any, proxyNames []string) []any { if len(groups) == 0 { return []any{ map[string]any{ "name": "Proxy", "type": "select", "proxies": append([]any{"DIRECT"}, stringsToAny(proxyNames)...), }, } } for index, item := range groups { group, ok := item.(map[string]any) if !ok { continue } group["proxies"] = appendUniqueAny(anySlice(group["proxies"]), stringsToAny(proxyNames)...) groups[index] = group } return groups } func anySlice(value any) []any { switch typed := value.(type) { case nil: return []any{} case []any: return append([]any{}, typed...) case []string: result := make([]any, 0, len(typed)) for _, item := range typed { result = append(result, item) } return result default: return []any{} } } func stringsToAny(values []string) []any { result := make([]any, 0, len(values)) for _, value := range values { result = append(result, value) } return result } func appendUniqueAny(base []any, values ...any) []any { existing := make(map[string]struct{}, len(base)) for _, item := range base { existing[fmt.Sprint(item)] = struct{}{} } for _, item := range values { key := fmt.Sprint(item) if _, ok := existing[key]; ok { continue } base = append(base, item) existing[key] = struct{}{} } return base } func generateClashFallback(servers []model.Server, user model.User) string { var builder strings.Builder builder.WriteString("proxies:\n") var proxyNames []string for _, s := range servers { conf := service.BuildNodeConfig(&s) proxy := buildClashProxy(conf, user) if proxy == nil { continue } switch proxy["type"] { case "ss": builder.WriteString(fmt.Sprintf( " - name: \"%s\"\n type: ss\n server: %s\n port: %d\n cipher: %s\n password: %s\n", conf.Name, conf.RawHost, conf.ServerPort, fmt.Sprint(proxy["cipher"]), user.UUID, )) case "vmess": builder.WriteString(fmt.Sprintf( " - name: \"%s\"\n type: vmess\n server: %s\n port: %d\n uuid: %s\n alterId: 0\n cipher: auto\n udp: true\n", conf.Name, conf.RawHost, conf.ServerPort, user.UUID, )) case "trojan": builder.WriteString(fmt.Sprintf( " - name: \"%s\"\n type: trojan\n server: %s\n port: %d\n password: %s\n udp: true\n", conf.Name, conf.RawHost, conf.ServerPort, user.UUID, )) } proxyNames = append(proxyNames, fmt.Sprintf("\"%s\"", conf.Name)) } builder.WriteString("\nproxy-groups:\n") builder.WriteString(" - name: Proxy\n type: select\n proxies:\n - DIRECT\n") for _, name := range proxyNames { builder.WriteString(" - " + name + "\n") } builder.WriteString("\nrules:\n") builder.WriteString(" - MATCH,Proxy\n") return builder.String() }