package generator import ( "fmt" "net/url" "os" "path/filepath" "sort" "strconv" "strings" yaml "gopkg.in/yaml.v3" "github.com/subconverter-go/internal/logging" "github.com/subconverter-go/internal/parser" ) // ClashGenerator Clash格式生成器 // 实现Clash代理配置的生成功能 type ClashGenerator struct { logger *logging.Logger } // NewClashGenerator 创建新的Clash生成器 // 返回初始化好的ClashGenerator实例 func NewClashGenerator(logger *logging.Logger) *ClashGenerator { return &ClashGenerator{ logger: logger, } } // Generate 生成Clash配置 func (g *ClashGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) { g.logger.Debugf("Generating Clash configuration") // 创建Clash配置结构 clashConfig := g.createClashConfig(configs, options) // 转换为YAML yamlData, err := yaml.Marshal(clashConfig) if err != nil { return "", fmt.Errorf("failed to marshal Clash configuration: %v", err) } return string(yamlData), nil } // ValidateOptions 验证Clash生成选项 func (g *ClashGenerator) ValidateOptions(options *GenerationOptions) error { g.logger.Debugf("Validating Clash generation options") // 验证基本信息 if options.Name == "" { return fmt.Errorf("configuration name is required") } // 验证模式 validModes := []string{"global", "rule", "direct"} if options.Mode != "" { valid := false for _, mode := range validModes { if options.Mode == mode { valid = true break } } if !valid { return fmt.Errorf("invalid mode: %s, must be one of: %s", options.Mode, strings.Join(validModes, ", ")) } } // 验证端口 if options.MixedPort < 0 || options.MixedPort > 65535 { return fmt.Errorf("invalid mixed port: %d", options.MixedPort) } // 验证日志级别 validLogLevels := []string{"info", "warning", "error", "debug", "silent"} if options.LogLevel != "" { valid := false for _, level := range validLogLevels { if options.LogLevel == level { valid = true break } } if !valid { return fmt.Errorf("invalid log level: %s, must be one of: %s", options.LogLevel, strings.Join(validLogLevels, ", ")) } } return nil } // GetSupportedFormats 获取支持的格式 func (g *ClashGenerator) GetSupportedFormats() []string { return []string{"clash"} } // ClashConfig Clash配置结构体 type ClashConfig struct { Port int `yaml:"port" json:"port"` SocksPort int `yaml:"socks-port" json:"socks-port"` AllowLan bool `yaml:"allow-lan" json:"allow-lan"` Mode string `yaml:"mode" json:"mode"` LogLevel string `yaml:"log-level" json:"log-level"` ExternalController string `yaml:"external-controller" json:"external-controller"` Secret string `yaml:"secret,omitempty" json:"secret,omitempty"` Proxies []map[string]interface{} `yaml:"proxies" json:"proxies"` ProxyGroup []map[string]interface{} `yaml:"proxy-groups" json:"proxy-groups"` Rules []string `yaml:"rules" json:"rules"` ProxyProviders map[string]map[string]interface{} `yaml:"proxy-providers,omitempty" json:"proxy-providers,omitempty"` } // createClashConfig 创建Clash配置结构 func (g *ClashGenerator) createClashConfig(configs []*parser.ProxyConfig, options *GenerationOptions) *ClashConfig { config := &ClashConfig{ Port: 7890, SocksPort: 7891, AllowLan: options.AllowLan, Mode: options.Mode, LogLevel: options.LogLevel, ExternalController: options.ExternalController, Secret: options.Secret, Proxies: make([]map[string]interface{}, 0), ProxyGroup: make([]map[string]interface{}, 0), Rules: make([]string, 0), } // 设置默认值 if config.Port == 0 { config.Port = 7890 } if config.SocksPort == 0 { config.SocksPort = 7891 } if config.Mode == "" { config.Mode = "rule" } if config.LogLevel == "" { config.LogLevel = "info" } // 生成代理配置 for _, proxyConfig := range configs { proxy := g.convertProxyConfig(proxyConfig) config.Proxies = append(config.Proxies, proxy) } // 生成代理组 config.ProxyGroup = g.createProxyGroups(configs, options) if len(options.GroupDefinitions) > 0 { for _, def := range options.GroupDefinitions { config.ProxyGroup = append(config.ProxyGroup, g.buildClashGroup(def, configs)) } } else if len(options.CustomGroups) > 0 { for _, entry := range options.CustomGroups { group, err := g.parseCustomGroup(entry, configs) if err != nil { g.logger.WithError(err).Warnf("Failed to parse custom group: %s", entry) continue } config.ProxyGroup = append(config.ProxyGroup, group) } } if len(options.Providers) > 0 { providers := make(map[string]map[string]interface{}, len(options.Providers)) for _, def := range options.Providers { providers[def.Name] = g.buildClashProvider(def) } config.ProxyProviders = providers } else if len(options.CustomProviders) > 0 { providers := make(map[string]map[string]interface{}) for _, entry := range options.CustomProviders { parsed, err := ParseProviderDefinition(entry) if err != nil { g.logger.WithError(err).Warnf("Failed to parse custom provider: %s", entry) continue } providers[parsed.Name] = g.buildClashProvider(parsed) } if len(providers) > 0 { config.ProxyProviders = providers } } // 生成规则 rules := g.createRules(options) config.Rules = g.applyCustomRulesets(rules, options) return config } // convertProxyConfig 转换代理配置为Clash格式 func (g *ClashGenerator) convertProxyConfig(proxyConfig *parser.ProxyConfig) map[string]interface{} { proxy := map[string]interface{}{ "name": proxyConfig.Name, "type": proxyConfig.Type, "server": proxyConfig.Server, "port": proxyConfig.Port, "udp": proxyConfig.UDP, } // 根据协议类型添加特定配置 switch proxyConfig.Protocol { case "ss": if method, exists := proxyConfig.Settings["method"]; exists { proxy["cipher"] = method } if password, exists := proxyConfig.Settings["password"]; exists { proxy["password"] = password } if plugin, exists := proxyConfig.Settings["plugin"]; exists { proxy["plugin"] = plugin } case "ssr": if method, exists := proxyConfig.Settings["method"]; exists { proxy["cipher"] = method } if password, exists := proxyConfig.Settings["password"]; exists { proxy["password"] = password } if protocol, exists := proxyConfig.Settings["protocol"]; exists { proxy["protocol"] = protocol } if obfs, exists := proxyConfig.Settings["obfs"]; exists { proxy["obfs"] = obfs } case "vmess": if uuid, exists := proxyConfig.Settings["uuid"]; exists { proxy["uuid"] = uuid } if alterId, exists := proxyConfig.Settings["alterId"]; exists { proxy["alterId"] = alterId } if network, exists := proxyConfig.Settings["network"]; exists { proxy["network"] = network } if tls, exists := proxyConfig.Settings["tls"]; exists { proxy["tls"] = tls } if host, exists := proxyConfig.Settings["host"]; exists { proxy["servername"] = host } if path, exists := proxyConfig.Settings["path"]; exists { proxy["ws-path"] = path } case "trojan": if password, exists := proxyConfig.Settings["password"]; exists { proxy["password"] = password } if sni, exists := proxyConfig.Settings["sni"]; exists { proxy["sni"] = sni } if network, exists := proxyConfig.Settings["network"]; exists { proxy["network"] = network } case "http", "https": if username, exists := proxyConfig.Settings["username"]; exists { proxy["username"] = username } if password, exists := proxyConfig.Settings["password"]; exists { proxy["password"] = password } if tls, exists := proxyConfig.Settings["tls"]; exists { proxy["tls"] = tls } case "socks5": if username, exists := proxyConfig.Settings["username"]; exists { proxy["username"] = username } if password, exists := proxyConfig.Settings["password"]; exists { proxy["password"] = password } } return proxy } // createProxyGroups 创建代理组 func (g *ClashGenerator) createProxyGroups(configs []*parser.ProxyConfig, options *GenerationOptions) []map[string]interface{} { groups := make([]map[string]interface{}, 0) // 创建代理名称列表 proxyNames := make([]string, 0) for _, config := range configs { proxyNames = append(proxyNames, config.Name) } // 创建选择代理组 selectGroup := map[string]interface{}{ "name": "PROXY", "type": "select", "proxies": proxyNames, } groups = append(groups, selectGroup) // 创建自动选择代理组(如果启用了代理测试) if options.ProxyTest { urlTestGroup := map[string]interface{}{ "name": "URL-TEST", "type": "url-test", "proxies": proxyNames, "url": options.ProxyURL, "interval": 300, "tolerance": 50, } if urlTestGroup["url"] == "" { urlTestGroup["url"] = "http://www.gstatic.com/generate_204" } groups = append(groups, urlTestGroup) } // 创建故障转移代理组 fallbackGroup := map[string]interface{}{ "name": "FALLBACK", "type": "fallback", "proxies": proxyNames, "url": "http://www.gstatic.com/generate_204", "interval": 300, } groups = append(groups, fallbackGroup) // 创建直连和拒绝代理组 groups = append(groups, map[string]interface{}{ "name": "DIRECT", "type": "select", "proxies": []string{"DIRECT"}, }) groups = append(groups, map[string]interface{}{ "name": "REJECT", "type": "select", "proxies": []string{"REJECT"}, }) return groups } func (g *ClashGenerator) buildClashGroup(def *GroupDefinition, configs []*parser.ProxyConfig) map[string]interface{} { group := map[string]interface{}{ "name": def.Name, "type": def.Type, } proxies := make([]string, 0, len(def.Proxies)) proxies = append(proxies, def.Proxies...) if len(proxies) == 0 && len(def.UseProviders) == 0 { for _, cfg := range configs { proxies = append(proxies, cfg.Name) } } if len(proxies) > 0 { group["proxies"] = proxies } if len(def.UseProviders) > 0 { group["use"] = def.UseProviders } if def.URL != "" { group["url"] = def.URL } if def.Interval != nil { group["interval"] = *def.Interval } if def.Timeout != nil { group["timeout"] = *def.Timeout } if def.Tolerance != nil { group["tolerance"] = *def.Tolerance } for k, v := range def.Extras { group[k] = v } return group } func (g *ClashGenerator) buildClashProvider(def *ProviderDefinition) map[string]interface{} { provider := map[string]interface{}{ "type": def.Type, "url": def.URL, "path": def.Path, } if def.Interval != nil { provider["interval"] = *def.Interval } if len(def.Fields) > 0 { keys := make([]string, 0, len(def.Fields)) for k := range def.Fields { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { provider[k] = def.Fields[k] } } if len(def.Flags) > 0 { keys := make([]string, 0, len(def.Flags)) for k := range def.Flags { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { provider[k] = def.Flags[k] } } if def.Health != nil { health := make(map[string]interface{}) if def.Health.Enable != nil { health["enable"] = *def.Health.Enable } if def.Health.URL != "" { health["url"] = def.Health.URL } if def.Health.Interval != nil { health["interval"] = *def.Health.Interval } if def.Health.Lazy != nil { health["lazy"] = *def.Health.Lazy } if def.Health.Tolerance != nil { health["tolerance"] = *def.Health.Tolerance } if def.Health.Timeout != nil { health["timeout"] = *def.Health.Timeout } if def.Health.Method != "" { health["method"] = def.Health.Method } if def.Health.Headers != "" { health["headers"] = def.Health.Headers } if len(health) > 0 { provider["health-check"] = health } } return provider } // createRules 创建规则 func (g *ClashGenerator) createRules(options *GenerationOptions) []string { rules := make([]string, 0) // 添加默认规则 if len(options.Rules) > 0 { rules = append(rules, options.Rules...) } else { // 添加默认规则集 defaultRules := []string{ "DOMAIN-SUFFIX,bilibili.com,DIRECT", "DOMAIN-SUFFIX,baidu.com,DIRECT", "DOMAIN-SUFFIX,cn,DIRECT", "DOMAIN-KEYWORD,google,PROXY", "DOMAIN-KEYWORD,github,PROXY", "DOMAIN-KEYWORD,twitter,PROXY", "DOMAIN-KEYWORD,facebook,PROXY", "DOMAIN-KEYWORD,youtube,PROXY", "DOMAIN-KEYWORD,instagram,PROXY", "DOMAIN-KEYWORD,telegram,PROXY", "GEOIP,CN,DIRECT", "IP-CIDR,127.0.0.0/8,DIRECT", "IP-CIDR,192.168.0.0/16,DIRECT", "IP-CIDR,10.0.0.0/8,DIRECT", "IP-CIDR,172.16.0.0/12,DIRECT", "MATCH,PROXY", } rules = append(rules, defaultRules...) } // 启用IPv6支持 if options.IPv6 { ipv6Rules := []string{ "IP-CIDR6,::1/128,DIRECT", "IP-CIDR6,fc00::/7,DIRECT", } rules = append(ipv6Rules, rules...) } // 启用局域网支持 if options.EnableLan { lanRules := []string{ "SRC-IP-CIDR,192.168.0.0/16,DIRECT", "SRC-IP-CIDR,10.0.0.0/8,DIRECT", "SRC-IP-CIDR,172.16.0.0/12,DIRECT", } rules = append(lanRules, rules...) } return rules } func (g *ClashGenerator) parseCustomGroup(definition string, configs []*parser.ProxyConfig) (map[string]interface{}, error) { values, err := url.ParseQuery(definition) if err != nil { return nil, fmt.Errorf("invalid group definition: %v", err) } name := values.Get("name") if name == "" { return nil, fmt.Errorf("custom group missing name") } groupType := values.Get("type") if groupType == "" { groupType = "select" } useProviders := make([]string, 0) if useStr := values.Get("use"); useStr != "" { for _, entry := range strings.Split(useStr, ",") { entry = strings.TrimSpace(entry) if entry != "" { useProviders = append(useProviders, entry) } } } proxies := make([]string, 0) if proxyStr := values.Get("proxies"); proxyStr != "" { for _, entry := range strings.Split(proxyStr, ",") { entry = strings.TrimSpace(entry) if entry != "" { proxies = append(proxies, entry) } } } if len(proxies) == 0 && len(useProviders) == 0 { for _, cfg := range configs { proxies = append(proxies, cfg.Name) } } group := map[string]interface{}{ "name": name, "type": groupType, "proxies": proxies, } if len(useProviders) > 0 { group["use"] = useProviders } if urlValue := values.Get("url"); urlValue != "" { group["url"] = urlValue } if interval := values.Get("interval"); interval != "" { if n, err := strconv.Atoi(interval); err == nil && n > 0 { group["interval"] = n } } if tolerance := values.Get("tolerance"); tolerance != "" { if n, err := strconv.Atoi(tolerance); err == nil && n >= 0 { group["tolerance"] = n } } if timeout := values.Get("timeout"); timeout != "" { if n, err := strconv.Atoi(timeout); err == nil && n >= 0 { group["timeout"] = n } } return group, nil } func (g *ClashGenerator) applyCustomRulesets(existing []string, options *GenerationOptions) []string { rules := make([]string, 0, len(existing)) for _, rule := range existing { rule = strings.TrimSpace(rule) if rule == "" { continue } rules = append(rules, rule) } for _, entry := range options.CustomRulesets { entry = strings.TrimSpace(entry) if entry == "" { continue } lower := strings.ToLower(entry) switch { case strings.HasPrefix(lower, "ruleset,"): payload := strings.TrimSpace(entry[len("ruleset,"):]) if payload == "" { continue } lines, err := g.loadRulesetLines(options.BasePath, payload) if err != nil { g.logger.WithError(err).Warnf("Failed to load ruleset %s", payload) continue } rules = append(rules, lines...) case strings.HasPrefix(lower, "rule,"): statement := strings.TrimSpace(entry[len("rule,"):]) if statement != "" { rules = append(rules, statement) } default: rules = append(rules, entry) } } return normalizeRules(rules) } func (g *ClashGenerator) loadRulesetLines(basePath, relative string) ([]string, error) { path := relative if basePath != "" && !filepath.IsAbs(relative) { path = filepath.Join(basePath, relative) } data, err := os.ReadFile(path) if err != nil { return nil, err } content := strings.ReplaceAll(string(data), "\r\n", "\n") lines := make([]string, 0) for _, line := range strings.Split(content, "\n") { trimmed := strings.TrimSpace(line) if trimmed == "" { continue } if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") { continue } lines = append(lines, trimmed) } return lines, nil } func normalizeRules(rules []string) []string { seen := make(map[string]struct{}) result := make([]string, 0, len(rules)) for _, rule := range rules { rule = strings.TrimSpace(rule) if rule == "" { continue } if _, ok := seen[rule]; ok { continue } seen[rule] = struct{}{} result = append(result, rule) } return result }