package conversion import ( "bytes" "compress/gzip" "compress/zlib" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" yaml "gopkg.in/yaml.v3" "github.com/subconverter-go/internal/generator" "github.com/subconverter-go/internal/logging" "github.com/subconverter-go/internal/parser" ) // EngineConversionRequest 引擎内部使用的转换请求 // 基于基础ConversionRequest,添加引擎需要的额外字段 type EngineConversionRequest struct { *ConversionRequest // 嵌入基础请求结构 // 引擎特定字段 InputText string `json:"inputText"` // 输入文本内容 Options *generator.GenerationOptions `json:"options"` // 生成选项 SkipInvalid bool `json:"skipInvalid"` // 是否跳过无效配置 } // EngineConversionResponse 引擎内部使用的转换响应 // 基于基础ConversionResponse,添加引擎需要的额外字段 type EngineConversionResponse struct { *ConversionResponse // 嵌入基础响应结构 // 引擎特定字段 Output string `json:"output"` // 转换后的配置内容 ProxyCount int `json:"proxyCount"` // 成功转换的代理数量 Errors []string `json:"errors"` // 详细错误列表 DebugInfo *DebugInfo `json:"debugInfo"` // 调试信息 } // DebugInfo 调试信息结构体 // 包含转换过程的调试数据 type DebugInfo struct { ParseTime int64 `json:"parseTime"` // 解析耗时(ms) GenerateTime int64 `json:"generateTime"` // 生成耗时(ms) TotalTime int64 `json:"totalTime"` // 总耗时(ms) InputSize int `json:"inputSize"` // 输入大小(bytes) OutputSize int `json:"outputSize"` // 输出大小(bytes) // 解析统计 ParseStats *ParseStats `json:"parseStats"` // 解析统计信息 // 生成统计 GenerateStats *GenerateStats `json:"generateStats"` // 生成统计信息 } // ParseStats 解析统计信息 type ParseStats struct { TotalInput int `json:"totalInput"` // 输入行数 ValidInput int `json:"validInput"` // 有效输入数 InvalidInput int `json:"invalidInput"` // 无效输入数 Protocols map[string]int `json:"protocols"` // 协议分布 } // GenerateStats 生成统计信息 type GenerateStats struct { GeneratedProxies int `json:"generatedProxies"` // 生成的代理数 GeneratedGroups int `json:"generatedGroups"` // 生成的代理组数 GeneratedRules int `json:"generatedRules"` // 生成的规则数 FormatFeatures map[string]bool `json:"formatFeatures"` // 格式特性 } // ConversionEngine 转换引擎 // 核心转换逻辑的协调器和执行器 type ConversionEngine struct { logger *logging.Logger parserMgr *parser.ParserManager generatorMgr *generator.GeneratorManager geoResolver GeoIPResolver geoipCache sync.Map httpClient *http.Client fetchCache sync.Map } type fetchCacheEntry struct { data string expires time.Time } const ( defaultFetchTimeout = 15 * time.Second fetchCacheTTL = 5 * time.Minute maxFetchBodySize int64 = 5 * 1024 * 1024 // 5MB safeguard ) // NewConversionEngine 创建新的转换引擎 // 返回初始化好的ConversionEngine实例 func NewConversionEngine( logger *logging.Logger, parserMgr *parser.ParserManager, generatorMgr *generator.GeneratorManager, ) *ConversionEngine { return &ConversionEngine{ logger: logger, parserMgr: parserMgr, generatorMgr: generatorMgr, geoResolver: newDefaultGeoIPResolver(logger), httpClient: &http.Client{ Timeout: defaultFetchTimeout, }, } } // SetHTTPClient allows tests or callers to provide a custom HTTP client. func (ce *ConversionEngine) SetHTTPClient(client *http.Client) { if client == nil { ce.httpClient = &http.Client{Timeout: defaultFetchTimeout} return } ce.httpClient = client } // Convert 执行转换操作 // 支持从URL或文本内容转换到目标格式 func (ce *ConversionEngine) Convert(request *ConversionRequest) (*ConversionResponse, error) { ce.logger.WithField("requestId", "engine-"+request.Target). WithField("targetFormat", request.Target). WithField("sourceUrl", request.SourceSummary()). Info("Starting conversion process") // 验证请求参数 if err := ce.validateRequest(request); err != nil { return ce.createErrorResponse(request, fmt.Sprintf("Invalid request: %v", err)), nil } // 记录开始时间 startTime := time.Now() // 解析输入配置 configs, parseResult, err := ce.parseInput(request) if err != nil { return ce.createErrorResponse(request, fmt.Sprintf("Failed to parse input: %v", err)), nil } // 如果没有有效的配置,返回错误 if len(configs) == 0 { return ce.createErrorResponse(request, "No valid proxy configurations found"), nil } filteredConfigs := ce.applyFilters(configs, request) if len(filteredConfigs) == 0 { return ce.createErrorResponse(request, "No proxy configurations matched requested filters"), nil } transformedConfigs, err := ce.applyTransformations(filteredConfigs, request) if err != nil { return ce.createErrorResponse(request, fmt.Sprintf("Invalid request: %v", err)), nil } finalConfigs, err := ce.applyFilterScript(transformedConfigs, request) if err != nil { return ce.createErrorResponse(request, fmt.Sprintf("Invalid request: %v", err)), nil } if len(finalConfigs) == 0 { return ce.createErrorResponse(request, "No proxy configurations remained after filtering"), nil } // 生成目标格式配置 output, generateResult, err := ce.generateOutput(request, finalConfigs) if err == nil { if uploadErr := ce.handleUpload(request, output); uploadErr != nil { return ce.createErrorResponse(request, fmt.Sprintf("Failed to upload content: %v", uploadErr)), nil } output = ce.applyManagedConfig(request, output) } if err != nil { return ce.createErrorResponse(request, fmt.Sprintf("Failed to generate output: %v", err)), nil } // 记录结束时间 endTime := time.Now() // 创建成功响应 response := ce.createSuccessResponse(request, output, finalConfigs, parseResult, generateResult) // 设置调试信息 response.DebugInfo = ce.createDebugInfo(request, startTime, endTime, parseResult, generateResult) ce.logger.WithField("requestId", "engine-"+request.Target). WithField("proxyCount", len(configs)). WithField("duration", endTime.Sub(startTime).Milliseconds()). Info("Conversion completed successfully") return response, nil } // validateRequest 验证转换请求 func (ce *ConversionEngine) validateRequest(request *ConversionRequest) error { if request == nil { return fmt.Errorf("request cannot be nil") } // 验证输入源 sources := request.GetSources() if len(sources) == 0 { return fmt.Errorf("source URL cannot be empty") } // 验证目标格式 - 使用现有的Target字段 if request.Target == "" { return fmt.Errorf("target format cannot be empty") } // 检查目标格式是否支持 supportedFormats := ce.generatorMgr.GetSupportedFormats() supported := false for _, format := range supportedFormats { if format == request.Target { supported = true break } } if !supported { return fmt.Errorf("unsupported target format: %s", request.Target) } return nil } // parseInput 解析输入配置 func (ce *ConversionEngine) parseInput(request *ConversionRequest) ([]*parser.ProxyConfig, *ParseResult, error) { var inputText string // 获取输入文本 sources := request.GetSources() if len(sources) > 0 { var builder strings.Builder for idx, src := range sources { text, fetchErr := ce.fetchFromURL(src, ce.selectUserAgent(request)) if fetchErr != nil { return nil, nil, fmt.Errorf("failed to fetch from URL %s: %v", src, fetchErr) } normalized := ce.normalizeSubscriptionContent(text) if normalized == "" { continue } builder.WriteString(normalized) if !strings.HasSuffix(normalized, "\n") { builder.WriteByte('\n') } if idx < len(sources)-1 { builder.WriteByte('\n') } } inputText = builder.String() } else { // 如果没有URL,使用默认的测试配置(简化实现) inputText = "ss://aes-256-cfb:password@192.168.1.1:8388" } // 解析配置 - 跳过无效配置 return ce.parseConfigurations(inputText, true) } // fetchFromURL loads subscription or helper content either from remote HTTP(S) // endpoints or local files. Results are cached briefly to avoid redundant // network calls when the same resource is requested multiple times within a // short window (mirroring the behaviour of the original service). func (ce *ConversionEngine) fetchFromURL(source string, userAgent string) (string, error) { if strings.TrimSpace(source) == "" { return "", fmt.Errorf("empty source URL") } cacheKey := ce.buildFetchCacheKey(source, userAgent) if cached, ok := ce.loadFetchCache(cacheKey); ok { return cached, nil } parsed, err := url.Parse(source) if err != nil { return "", fmt.Errorf("invalid URL: %w", err) } var ( data []byte readErr error ) switch strings.ToLower(parsed.Scheme) { case "http", "https": data, readErr = ce.fetchHTTPResource(source, userAgent) case "file": resolved := parsed.Path if resolved == "" { resolved = parsed.Host } data, readErr = os.ReadFile(resolved) default: if parsed.Scheme != "" { readErr = fmt.Errorf("unsupported URL scheme: %s", parsed.Scheme) } else { resolved := source if !filepath.IsAbs(resolved) && parsed.Path != "" { resolved = parsed.Path } data, readErr = os.ReadFile(resolved) } } if readErr != nil { return "", readErr } content := string(data) ce.storeFetchCache(cacheKey, content) return content, nil } func (ce *ConversionEngine) buildFetchCacheKey(source, userAgent string) string { key := strings.TrimSpace(source) ua := strings.TrimSpace(userAgent) if ua == "" { return key } return key + "::ua=" + ua } func (ce *ConversionEngine) selectUserAgent(request *ConversionRequest) string { if request != nil { if ua := strings.TrimSpace(request.UserAgent); ua != "" { return ua } switch strings.ToLower(strings.TrimSpace(request.Target)) { case "clash", "clashr": return "ClashForWindows/0.20.0" case "surge": return "Surge/1109" case "quanx", "quantumult-x": return "Quantumult%20X/1.2.0" case "loon": return "Loon/2.1.2" case "surfboard": return "Surfboard/1.6.0" case "v2ray": return "v2rayN/6.30" } } return "subconverter-go/1.0" } func (ce *ConversionEngine) normalizeSubscriptionContent(raw string) string { return ce.normalizeSubscriptionContentWithDepth(raw, 0) } func (ce *ConversionEngine) normalizeSubscriptionContentWithDepth(raw string, depth int) string { trimmed := strings.TrimSpace(raw) if trimmed == "" { return raw } if depth > 4 { return trimmed } if ce.looksLikeClashYAML(trimmed) { if converted, ok := ce.convertClashYAML(trimmed); ok { return converted } } if ce.containsProxyIndicators(trimmed) { return trimmed } decoded := ce.decodeBase64Payload(trimmed) if decoded != "" && decoded != raw { return ce.normalizeSubscriptionContentWithDepth(decoded, depth+1) } return trimmed } func (ce *ConversionEngine) containsProxyIndicators(content string) bool { lower := strings.ToLower(content) if strings.Contains(lower, "vmess://") || strings.Contains(lower, "ss://") || strings.Contains(lower, "trojan://") || strings.Contains(lower, "vless://") || strings.Contains(lower, "hysteria://") || strings.Contains(lower, "ssr://") { return true } if strings.Contains(lower, "proxies:") && strings.Contains(lower, "proxy-groups:") { return true } return false } func (ce *ConversionEngine) decodeBase64Payload(raw string) string { clean := make([]rune, 0, len(raw)) for _, r := range raw { if r == '\n' || r == '\r' || r == '\t' || r == ' ' { continue } clean = append(clean, r) } if len(clean) == 0 { return "" } data, ok := ce.tryDecodeBase64(string(clean)) if !ok || len(data) == 0 { return "" } plain, err := ce.maybeDecompress(data) if err == nil { data = plain } if !isLikelyText(data) { return "" } return string(data) } func (ce *ConversionEngine) tryDecodeBase64(clean string) ([]byte, bool) { paddingNeeded := len(clean) % 4 if paddingNeeded != 0 { clean += strings.Repeat("=", 4-paddingNeeded) } for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.URLEncoding} { decoded, err := enc.DecodeString(clean) if err == nil && len(decoded) > 0 { return decoded, true } } return nil, false } func (ce *ConversionEngine) maybeDecompress(data []byte) ([]byte, error) { if len(data) < 2 { return data, nil } if bytes.HasPrefix(data, []byte{0x1f, 0x8b}) { gr, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } defer gr.Close() return io.ReadAll(gr) } if data[0] == 0x78 && (data[1] == 0x01 || data[1] == 0x9c || data[1] == 0xda) { zr, err := zlib.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } defer zr.Close() return io.ReadAll(zr) } return data, nil } func isLikelyText(data []byte) bool { if len(data) == 0 { return false } var printable int for _, b := range data { switch { case b == 0x0a || b == 0x0d || b == 0x09: printable++ case b >= 0x20 && b <= 0x7e: printable++ case b >= 0x80: printable++ default: return false } } return float64(printable)/float64(len(data)) > 0.7 } func (ce *ConversionEngine) looksLikeClashYAML(content string) bool { lower := strings.ToLower(content) return strings.Contains(lower, "proxies:") && (strings.Contains(lower, "proxy-groups:") || strings.Contains(lower, "proxy-groups:") || strings.Contains(lower, "rules:")) } func (ce *ConversionEngine) convertClashYAML(content string) (string, bool) { var doc struct { Proxies []map[string]interface{} `yaml:"proxies"` } if err := yaml.Unmarshal([]byte(content), &doc); err != nil { return "", false } if len(doc.Proxies) == 0 { return "", false } builder := strings.Builder{} for _, proxy := range doc.Proxies { share, ok := ce.shareURIFromClashProxy(proxy) if !ok || strings.TrimSpace(share) == "" { continue } builder.WriteString(share) builder.WriteByte('\n') } result := strings.TrimSpace(builder.String()) if result == "" { return "", false } return result, true } func (ce *ConversionEngine) shareURIFromClashProxy(proxy map[string]interface{}) (string, bool) { typeVal := strings.ToLower(getString(proxy, "type")) name := getString(proxy, "name") if name == "" { name = getString(proxy, "ps") } server := getString(proxy, "server") port, ok := getInt(proxy, "port") if !ok { return "", false } switch typeVal { case "ss", "shadowsocks": return buildShadowsocksURI(proxy, name, server, port) case "vmess": return buildVmessURI(proxy, name, server, port) case "trojan": return buildTrojanURI(proxy, name, server, port) case "socks5", "socks": return buildSocksURI(proxy, name, server, port) case "http", "https": return buildHTTPURI(proxy, name, server, port) default: return "", false } } func buildShadowsocksURI(proxy map[string]interface{}, name, server string, port int) (string, bool) { cipher := getString(proxy, "cipher") if cipher == "" { cipher = getString(proxy, "method") } password := getString(proxy, "password") if cipher == "" || password == "" || server == "" { return "", false } userInfo := fmt.Sprintf("%s:%s", cipher, password) encoded := base64.StdEncoding.EncodeToString([]byte(userInfo)) encoded = strings.TrimRight(encoded, "=") uri := fmt.Sprintf("ss://%s@%s:%d", encoded, server, port) plugin := getString(proxy, "plugin") if plugin != "" { params := url.Values{} pluginOpts := fetchPluginOptions(proxy) params.Set("plugin", pluginOpts) uri += "?" + params.Encode() } if name != "" { uri += "#" + url.QueryEscape(name) } return uri, true } func fetchPluginOptions(proxy map[string]interface{}) string { plugin := getString(proxy, "plugin") if plugin == "" { return "" } pluginOpts := proxy["plugin-opts"] switch v := pluginOpts.(type) { case string: if strings.TrimSpace(v) == "" { return plugin } return fmt.Sprintf("%s;%s", plugin, v) case map[string]interface{}: parts := make([]string, 0, len(v)+1) parts = append(parts, plugin) for key, value := range v { parts = append(parts, fmt.Sprintf("%s=%v", key, value)) } return strings.Join(parts, ";") default: return plugin } } func buildVmessURI(proxy map[string]interface{}, name, server string, port int) (string, bool) { uuid := getString(proxy, "uuid") if uuid == "" { uuid = getString(proxy, "id") } if uuid == "" || server == "" { return "", false } alterID, _ := getInt(proxy, "alterId") if alterID == 0 { alterID, _ = getInt(proxy, "alterid") } scy := getString(proxy, "cipher") if scy == "" { scy = getString(proxy, "security") } if scy == "" { scy = "auto" } network := strings.ToLower(getString(proxy, "network")) if network == "" { network = "tcp" } path := getString(proxy, "path") if path == "" { if wsOpts, ok := proxy["ws-opts"].(map[string]interface{}); ok { path = getString(wsOpts, "path") } } host := getString(proxy, "host") if host == "" { if headers, ok := proxy["ws-opts"].(map[string]interface{}); ok { if hMap, ok := headers["headers"].(map[string]interface{}); ok { host = getString(hMap, "Host") } } } tlsField := proxy["tls"] tls := "" switch v := tlsField.(type) { case bool: if v { tls = "tls" } case string: if strings.EqualFold(v, "tls") || strings.EqualFold(v, "true") { tls = "tls" } } sni := getString(proxy, "sni") if sni == "" { sni = getString(proxy, "servername") } var alpn []string if rawAlpn, ok := proxy["alpn"].([]interface{}); ok { for _, item := range rawAlpn { if s, ok := item.(string); ok && s != "" { alpn = append(alpn, s) } } } settings := map[string]interface{}{ "v": "2", "ps": name, "add": server, "port": fmt.Sprintf("%d", port), "id": uuid, "aid": fmt.Sprintf("%d", alterID), "scy": scy, "net": network, "type": "none", "host": host, "path": path, "tls": tls, } if sni != "" { settings["sni"] = sni } if len(alpn) > 0 { settings["alpn"] = strings.Join(alpn, ",") } if fp := getString(proxy, "client-fingerprint"); fp != "" { settings["fp"] = fp } jsonBytes, err := json.Marshal(settings) if err != nil { return "", false } encoded := base64.StdEncoding.EncodeToString(jsonBytes) return fmt.Sprintf("vmess://%s", encoded), true } func buildTrojanURI(proxy map[string]interface{}, name, server string, port int) (string, bool) { password := getString(proxy, "password") if password == "" || server == "" { return "", false } params := url.Values{} sni := getString(proxy, "sni") if sni == "" { sni = getString(proxy, "servername") } if sni != "" { params.Set("sni", sni) } alpn := proxy["alpn"] switch v := alpn.(type) { case string: if v != "" { params.Set("alpn", v) } case []interface{}: slices := make([]string, 0, len(v)) for _, item := range v { if s, ok := item.(string); ok && s != "" { slices = append(slices, s) } } if len(slices) > 0 { params.Set("alpn", strings.Join(slices, ",")) } } if network := getString(proxy, "network"); network != "" { params.Set("type", network) } uri := fmt.Sprintf("trojan://%s@%s:%d", password, server, port) if len(params) > 0 { uri += "?" + params.Encode() } if name != "" { uri += "#" + url.QueryEscape(name) } return uri, true } func buildSocksURI(proxy map[string]interface{}, name, server string, port int) (string, bool) { if server == "" { return "", false } user := getString(proxy, "username") if user == "" { user = getString(proxy, "user") } pass := getString(proxy, "password") var auth string if user != "" || pass != "" { auth = url.UserPassword(user, pass).String() + "@" } uri := fmt.Sprintf("socks5://%s%s:%d", auth, server, port) if name != "" { uri += "#" + url.QueryEscape(name) } return uri, true } func buildHTTPURI(proxy map[string]interface{}, name, server string, port int) (string, bool) { if server == "" { return "", false } user := getString(proxy, "username") if user == "" { user = getString(proxy, "user") } pass := getString(proxy, "password") var auth string if user != "" || pass != "" { auth = url.UserPassword(user, pass).String() + "@" } scheme := "http" if tlsEnabled(proxy) { scheme = "https" } uri := fmt.Sprintf("%s://%s%s:%d", scheme, auth, server, port) if name != "" { uri += "#" + url.QueryEscape(name) } return uri, true } func tlsEnabled(proxy map[string]interface{}) bool { if proxy == nil { return false } if v, ok := proxy["tls"].(bool); ok { return v } if s := strings.ToLower(getString(proxy, "tls")); s == "tls" || s == "true" { return true } return false } func getString(m map[string]interface{}, key string) string { if m == nil { return "" } if val, ok := m[key]; ok { switch v := val.(type) { case string: return v case fmt.Stringer: return v.String() case []byte: return string(v) case int, int64, float64: return fmt.Sprintf("%v", v) } } return "" } func getInt(m map[string]interface{}, key string) (int, bool) { if m == nil { return 0, false } if val, ok := m[key]; ok { switch v := val.(type) { case int: return v, true case int64: return int(v), true case float64: return int(v), true case string: if v == "" { return 0, false } i, err := strconv.Atoi(v) if err != nil { return 0, false } return i, true } } return 0, false } func (ce *ConversionEngine) fetchHTTPResource(source string, userAgent string) ([]byte, error) { client := ce.httpClient if client == nil { client = &http.Client{Timeout: defaultFetchTimeout} } req, err := http.NewRequest(http.MethodGet, source, nil) if err != nil { return nil, err } ua := strings.TrimSpace(userAgent) if ua == "" { ua = "subconverter-go/1.0" } req.Header.Set("User-Agent", ua) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { return nil, fmt.Errorf("fetch failed with status %d", resp.StatusCode) } reader := io.LimitReader(resp.Body, maxFetchBodySize+1) data, err := io.ReadAll(reader) if err != nil { return nil, err } if int64(len(data)) > maxFetchBodySize { return nil, errors.New("remote content exceeds allowed size") } return data, nil } func (ce *ConversionEngine) loadFetchCache(key string) (string, bool) { if value, ok := ce.fetchCache.Load(key); ok { if entry, ok := value.(fetchCacheEntry); ok { if time.Now().Before(entry.expires) { return entry.data, true } ce.fetchCache.Delete(key) } } return "", false } func (ce *ConversionEngine) storeFetchCache(key, data string) { ce.fetchCache.Store(key, fetchCacheEntry{ data: data, expires: time.Now().Add(fetchCacheTTL), }) } // parseConfigurations 解析配置文本 func (ce *ConversionEngine) parseConfigurations(inputText string, skipInvalid bool) ([]*parser.ProxyConfig, *ParseResult, error) { result := &ParseResult{ ValidConfigs: make([]*parser.ProxyConfig, 0), InvalidInputs: make([]string, 0), ErrorMessages: make([]string, 0), ProtocolStats: make(map[string]int), } // 分割输入文本 lines := ce.splitInputLines(inputText) for i, line := range lines { line = ce.trimLine(line) if line == "" { continue } // 尝试解析每一行 config, err := ce.parseSingleLine(line, i+1) if err != nil { result.InvalidInputs = append(result.InvalidInputs, line) result.ErrorMessages = append(result.ErrorMessages, fmt.Sprintf("Line %d: %v", i+1, err)) if !skipInvalid { return nil, result, fmt.Errorf("failed to parse line %d: %v", i+1, err) } continue } if config != nil { result.ValidConfigs = append(result.ValidConfigs, config) result.ProtocolStats[config.Protocol]++ } } ce.logger.WithField("validConfigs", len(result.ValidConfigs)). WithField("invalidInputs", len(result.InvalidInputs)). Info("Configuration parsing completed") return result.ValidConfigs, result, nil } // splitInputLines 分割输入文本为行 func (ce *ConversionEngine) splitInputLines(input string) []string { // 按行分割输入文本 lines := strings.Split(strings.TrimSpace(input), "\n") // 过滤空行 result := make([]string, 0) for _, line := range lines { if trimmed := strings.TrimSpace(line); trimmed != "" { result = append(result, trimmed) } } return result } // trimLine 清理行内容 func (ce *ConversionEngine) trimLine(line string) string { trimmed := strings.TrimSpace(line) if trimmed == "" { return "" } // 移除可能存在的 BOM 或零宽度字符 trimmed = strings.TrimLeft(trimmed, "\ufeff\u200b\u200c\u200d\u2060") trimmed = strings.TrimSpace(trimmed) if trimmed == "" { return "" } // 跳过整行注释 switch { case strings.HasPrefix(trimmed, "#"), strings.HasPrefix(trimmed, ";"), strings.HasPrefix(trimmed, "//"): return "" } return trimmed } // parseSingleLine 解析单行配置 func (ce *ConversionEngine) parseSingleLine(line string, lineNumber int) (*parser.ProxyConfig, error) { // 尝试自动检测协议并解析 protocol := ce.detectProtocol(line) if protocol == "" { return nil, fmt.Errorf("unable to detect protocol for line: %s", line) } // 获取对应的解析器 parser, err := ce.parserMgr.GetParser(protocol) if err != nil { return nil, fmt.Errorf("no parser available for protocol: %s", protocol) } // 解析配置 config, err := parser.Parse(line) if err != nil { return nil, fmt.Errorf("failed to parse %s configuration: %v", protocol, err) } // 验证配置 if err := parser.Validate(config); err != nil { return nil, fmt.Errorf("invalid %s configuration: %v", protocol, err) } return config, nil } // detectProtocol 检测配置协议 func (ce *ConversionEngine) detectProtocol(line string) string { candidate := strings.TrimSpace(line) if candidate == "" { return "" } lower := strings.ToLower(candidate) switch { case strings.HasPrefix(lower, "ssr://"): return "ssr" case strings.HasPrefix(lower, "vmess://"), strings.HasPrefix(lower, "vmess1://"): return "vmess" case strings.HasPrefix(lower, "ss://"): return "ss" case strings.HasPrefix(lower, "socks5://"), strings.HasPrefix(lower, "socks://"), strings.HasPrefix(lower, "https://t.me/socks"), strings.HasPrefix(lower, "tg://socks"): return "socks5" case strings.HasPrefix(lower, "trojan://"): return "trojan" case strings.HasPrefix(lower, "http://"), strings.HasPrefix(lower, "https://"), strings.HasPrefix(lower, "tg://http"), strings.HasPrefix(lower, "https://t.me/http"): return "http" } // Fallback: 尝试在字符串中查找协议标识 if strings.Contains(lower, "ssr://") { return "ssr" } if strings.Contains(lower, "vmess://") || strings.Contains(lower, "vmess1://") { return "vmess" } if strings.Contains(lower, "ss://") { return "ss" } if strings.Contains(lower, "trojan://") { return "trojan" } if strings.Contains(lower, "socks5://") || strings.Contains(lower, "socks://") { return "socks5" } if strings.Contains(lower, "http://") || strings.Contains(lower, "https://") { return "http" } return "" } // generateOutput 生成输出配置 func (ce *ConversionEngine) generateOutput(request *ConversionRequest, configs []*parser.ProxyConfig) (string, *GenerateResult, error) { result := &GenerateResult{ GeneratedOutput: "", FormatFeatures: make(map[string]bool), } if request.List { lines := make([]string, 0, len(configs)) for _, cfg := range configs { name := strings.TrimSpace(cfg.Name) if name == "" { if cfg.Server != "" && cfg.Port != 0 { name = fmt.Sprintf("%s:%d", cfg.Server, cfg.Port) } else { name = cfg.Type } } lines = append(lines, name) } output := strings.Join(lines, "\n") result.GeneratedOutput = output result.FormatFeatures["list_mode"] = true return output, result, nil } // 获取对应的生成器 gen, err := ce.generatorMgr.GetGenerator(request.Target) if err != nil { return "", result, fmt.Errorf("no generator available for format: %s", request.Target) } // 创建默认的生成选项 options := &generator.GenerationOptions{ Name: "SubConverter-Go", Group: request.Group, IPv6: request.IPv6, Mode: "rule", BasePath: request.BasePath, } options.AppendType = request.AppendType options.TFO = request.TFO options.Script = request.Script options.SkipCert = request.SkipCert options.FilterDeprecated = request.FilterDeprecated options.ExpandRules = request.ExpandRules options.AppendInfo = request.AppendInfo options.Prepend = request.Prepend options.Classic = request.Classic options.TLS13 = request.TLS13 options.AddEmoji = request.AddEmoji options.RemoveEmoji = request.RemoveEmoji options.RenameRules = append([]string{}, request.RenameRules...) options.CustomGroups = append([]string{}, request.Groups...) options.CustomRulesets = append([]string{}, request.Rulesets...) options.CustomProviders = append([]string{}, request.Providers...) if len(request.Providers) > 0 { providers := make([]*generator.ProviderDefinition, 0, len(request.Providers)) for _, entry := range request.Providers { parsed, err := generator.ParseProviderDefinition(entry) if err != nil { return "", result, fmt.Errorf("invalid provider definition: %w", err) } providers = append(providers, parsed) } options.Providers = providers } if len(request.Groups) > 0 { groups := make([]*generator.GroupDefinition, 0, len(request.Groups)) for _, entry := range request.Groups { parsed, err := generator.ParseGroupDefinition(entry) if err != nil { return "", result, fmt.Errorf("invalid group definition: %w", err) } groups = append(groups, parsed) } options.GroupDefinitions = groups } options.FilterScript = request.FilterScript options.Upload = request.Upload options.UploadPath = request.UploadPath options.DeviceID = request.DeviceID options.Interval = request.Interval // 验证生成选项 if err := gen.ValidateOptions(options); err != nil { return "", result, fmt.Errorf("invalid generation options: %v", err) } // 生成配置 output, err := gen.Generate(configs, options) if err != nil { return "", result, fmt.Errorf("failed to generate %s configuration: %v", request.Target, err) } result.GeneratedOutput = output result.FormatFeatures["success"] = true result.GenerateStats = &GenerateStats{ GeneratedProxies: len(configs), GeneratedGroups: len(options.GroupDefinitions), GeneratedRules: len(options.CustomRulesets), FormatFeatures: copyFormatFeatures(result.FormatFeatures), } return output, result, nil } func (ce *ConversionEngine) applyFilters(configs []*parser.ProxyConfig, request *ConversionRequest) []*parser.ProxyConfig { if len(request.Include) == 0 && len(request.Exclude) == 0 { return configs } includeTokens := make([]string, 0, len(request.Include)) for _, token := range request.Include { token = strings.ToLower(strings.TrimSpace(token)) if token != "" { includeTokens = append(includeTokens, token) } } excludeTokens := make([]string, 0, len(request.Exclude)) for _, token := range request.Exclude { token = strings.ToLower(strings.TrimSpace(token)) if token != "" { excludeTokens = append(excludeTokens, token) } } base := make([]*parser.ProxyConfig, 0, len(configs)) for _, cfg := range configs { label := strings.ToLower(strings.TrimSpace(firstNonEmpty(cfg.Remarks, cfg.Name))) if label == "" { if cfg.Server != "" && cfg.Port != 0 { label = fmt.Sprintf("%s:%d", strings.ToLower(cfg.Server), cfg.Port) } else { label = strings.ToLower(cfg.Type) } } if matchesAny(label, excludeTokens) { continue } base = append(base, cfg) } filtered := base if len(includeTokens) > 0 { included := make([]*parser.ProxyConfig, 0, len(base)) for _, cfg := range base { label := strings.ToLower(strings.TrimSpace(firstNonEmpty(cfg.Remarks, cfg.Name))) if label == "" { if cfg.Server != "" && cfg.Port != 0 { label = fmt.Sprintf("%s:%d", strings.ToLower(cfg.Server), cfg.Port) } else { label = strings.ToLower(cfg.Type) } } if matchesAny(label, includeTokens) { included = append(included, cfg) } } if len(included) > 0 { filtered = included } } ce.logger.WithField("original", len(configs)). WithField("after_exclude", len(base)). WithField("filtered", len(filtered)). Debug("Applied include/exclude filters to proxy list") return filtered } func (ce *ConversionEngine) applyTransformations(configs []*parser.ProxyConfig, request *ConversionRequest) ([]*parser.ProxyConfig, error) { result := make([]*parser.ProxyConfig, 0, len(configs)) renameRules, err := ce.compileRenameRules(request) if err != nil { return nil, err } emojiRules, err := ce.compileEmojiRules(request) if err != nil { return nil, err } needsRename := len(renameRules) > 0 needsRemove := request.RemoveEmoji needsEmoji := request.AddEmoji || len(emojiRules) > 0 if !needsRename && !needsRemove && !needsEmoji { return configs, nil } for idx, cfg := range configs { clone := *cfg clone.Settings = copySettings(cfg.Settings) remark := firstNonEmpty(cfg.Remarks, cfg.Name) if remark == "" { remark = cfg.Name } name := cfg.Name node := newScriptNode(cfg, remark, request.Group, idx) if needsRename { updated, renameErr := applyRenameRules(renameRules, &node, ce.logger) if renameErr != nil { return nil, renameErr } if trimmed := strings.TrimSpace(updated); trimmed != "" { remark = trimmed name = trimmed node.Remark = trimmed } } if needsRemove { remark = removeLeadingEmoji(remark) name = removeLeadingEmoji(name) node.Remark = remark } emoji := "" if len(emojiRules) > 0 { e, emojiErr := applyEmojiRules(emojiRules, &node, ce.logger) if emojiErr != nil { return nil, emojiErr } emoji = strings.TrimSpace(e) } if emoji == "" && request.AddEmoji { emoji = ce.pickEmoji(clone, request) } if emoji != "" { remark = prependEmoji(emoji, remark) name = prependEmoji(emoji, name) } clone.Name = strings.TrimSpace(name) clone.Remarks = strings.TrimSpace(remark) result = append(result, &clone) } return result, nil } func (ce *ConversionEngine) applyFilterScript(configs []*parser.ProxyConfig, request *ConversionRequest) ([]*parser.ProxyConfig, error) { script, err := ce.prepareFilterScript(request.FilterScript, request.BasePath, request) if err != nil { return nil, err } if script == "" { return configs, nil } runner, err := ce.newScriptRunner(script, request.BasePath, filterFunctionName) if err != nil { return nil, err } filtered := make([]*parser.ProxyConfig, 0, len(configs)) for idx, cfg := range configs { remark := firstNonEmpty(cfg.Remarks, cfg.Name) if remark == "" { remark = cfg.Name } node := newScriptNode(cfg, remark, request.Group, idx) shouldRemove, callErr := runner.callBool(node) if callErr != nil { ce.logger.WithError(callErr).Warn("filter script execution failed") return nil, fmt.Errorf("filter script error: %w", callErr) } if !shouldRemove { filtered = append(filtered, cfg) } } return filtered, nil } func (ce *ConversionEngine) prepareFilterScript(raw string, basePath string, request *ConversionRequest) (string, error) { script := strings.TrimSpace(raw) if script == "" { return "", nil } lower := strings.ToLower(script) switch { case strings.HasPrefix(lower, "!!script:"): return strings.TrimSpace(script[len("!!script:"):]), nil case strings.HasPrefix(lower, "!!import:"): content, _, err := ce.loadImportResource(script[len("!!import:"):], basePath, request) if err != nil { return "", err } return strings.TrimSpace(content), nil case strings.HasPrefix(lower, "path:"): return script, nil case strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://"): content, err := ce.fetchFromURL(script, ce.selectUserAgent(request)) if err != nil { return "", err } return strings.TrimSpace(content), nil } if strings.Contains(script, "function ") { return script, nil } resolved := script if basePath != "" && !filepath.IsAbs(resolved) { resolved = filepath.Join(basePath, resolved) } if _, err := os.Stat(resolved); err == nil { return "path:" + script, nil } ce.logger.Warnf("Filter script %s not found; skipping", script) return "", nil } func (ce *ConversionEngine) handleUpload(request *ConversionRequest, content string) error { if !request.Upload { return nil } path := strings.TrimSpace(request.UploadPath) if path == "" { ce.logger.Warn("Upload requested without upload_path; skipping") return nil } lower := strings.ToLower(path) switch { case strings.HasPrefix(lower, "file://"): parsed, err := url.Parse(path) if err != nil { return err } resolved := parsed.Path if resolved == "" { resolved = parsed.Host } return ce.uploadToFile(resolved, content) case strings.HasPrefix(lower, "http://"), strings.HasPrefix(lower, "https://"): return ce.uploadOverHTTP(path, content) default: resolved := path if request.BasePath != "" && !filepath.IsAbs(resolved) { resolved = filepath.Join(request.BasePath, resolved) } return ce.uploadToFile(resolved, content) } } func (ce *ConversionEngine) uploadToFile(destination string, content string) error { if destination == "" { return fmt.Errorf("upload destination is empty") } dir := filepath.Dir(destination) if err := os.MkdirAll(dir, 0o755); err != nil { return err } return os.WriteFile(destination, []byte(content), 0o644) } func (ce *ConversionEngine) uploadOverHTTP(target string, content string) error { client := ce.httpClient if client == nil { client = &http.Client{Timeout: defaultFetchTimeout} } methods := []string{http.MethodPut, http.MethodPost} for idx, method := range methods { req, err := http.NewRequest(method, target, strings.NewReader(content)) if err != nil { return err } req.Header.Set("Content-Type", "text/plain; charset=utf-8") resp, err := client.Do(req) if err != nil { if idx == len(methods)-1 { return err } continue } _, _ = io.Copy(io.Discard, resp.Body) resp.Body.Close() if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { return nil } if idx == len(methods)-1 { return fmt.Errorf("upload failed with status %d", resp.StatusCode) } } return nil } func (ce *ConversionEngine) pickEmoji(cfg parser.ProxyConfig, request *ConversionRequest) string { switch strings.ToLower(cfg.Protocol) { case "ss", "ssr": return "🛰️" case "vmess": return "🛸" case "trojan": return "🐴" } host := strings.ToLower(cfg.Server) switch { case strings.Contains(host, "hk"): return "🇭🇰" case strings.Contains(host, "jp"): return "🇯🇵" case strings.Contains(host, "us") || strings.HasSuffix(host, ".us"): return "🇺🇸" case strings.Contains(host, "sg"): return "🇸🇬" case strings.Contains(host, "kr"): return "🇰🇷" case strings.Contains(host, "tw"): return "🇹🇼" } return "" } func removeLeadingEmoji(input string) string { trimmed := strings.TrimSpace(input) if trimmed == "" { return trimmed } runes := []rune(trimmed) if len(runes) == 0 { return trimmed } skip := leadingEmojiLength(runes) if skip == 0 { return trimmed } return strings.TrimSpace(string(runes[skip:])) } func prependEmoji(emoji string, value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return emoji } if strings.HasPrefix(trimmed, emoji) { return trimmed } return strings.TrimSpace(emoji + " " + trimmed) } func isEmojiRune(r rune) bool { return r >= 0x1F300 && r <= 0x1FAFF } func isRegionalIndicator(r rune) bool { return r >= 0x1F1E6 && r <= 0x1F1FF } func isVariationSelector(r rune) bool { return r >= 0xFE00 && r <= 0xFE0F } func leadingEmojiLength(runes []rune) int { if len(runes) == 0 { return 0 } first := runes[0] if isEmojiRune(first) { skip := 1 if len(runes) > skip && isVariationSelector(runes[skip]) { skip++ } return skip } if isRegionalIndicator(first) { skip := 1 if len(runes) > skip && isRegionalIndicator(runes[skip]) { skip++ } return skip } return 0 } func matchesAny(label string, tokens []string) bool { for _, token := range tokens { if token != "" && strings.Contains(label, token) { return true } } return false } func copyFormatFeatures(features map[string]bool) map[string]bool { if len(features) == 0 { return map[string]bool{} } dup := make(map[string]bool, len(features)) for k, v := range features { dup[k] = v } return dup } func firstNonEmpty(values ...string) string { for _, v := range values { if strings.TrimSpace(v) != "" { return v } } return "" } // createSuccessResponse 创建成功响应 func (ce *ConversionEngine) createSuccessResponse( request *ConversionRequest, output string, configs []*parser.ProxyConfig, parseResult *ParseResult, generateResult *GenerateResult, ) *ConversionResponse { response := NewConversionResponse() response.Success = true response.Content = output response.ContentType = request.GetContentType() response.TargetFormat = request.Target response.SourceURL = request.SourceSummary() response.NodeCount = len(configs) response.ProcessingTime = 0 // 临时设置为0,实际应该在调用处计算 response.Timestamp = time.Now() if headerVal := ce.profileUpdateHeader(request.Interval); headerVal != "" { response.AddHeader("profile-update-interval", headerVal) } // 添加调试信息 debugInfo := map[string]interface{}{ "parse_time": 0, "generate_time": 0, "total_time": 0, "input_size": 0, "output_size": len(output), "proxy_count": len(configs), "error_count": len(parseResult.ErrorMessages), "protocols": parseResult.ProtocolStats, "format_features": generateResult.FormatFeatures, } if generateResult.GenerateStats != nil { debugInfo["generate_stats"] = map[string]interface{}{ "generated_proxies": generateResult.GenerateStats.GeneratedProxies, "generated_groups": generateResult.GenerateStats.GeneratedGroups, "generated_rules": generateResult.GenerateStats.GeneratedRules, "features": generateResult.GenerateStats.FormatFeatures, } } response.DebugInfo = debugInfo return response } // createErrorResponse 创建错误响应 func (ce *ConversionEngine) createErrorResponse(request *ConversionRequest, errorMessage string) *ConversionResponse { response := NewConversionResponse() response.Success = false response.Error = errorMessage response.ErrorCode = "VALIDATION_ERROR" response.ErrorDetail = errorMessage response.TargetFormat = request.Target response.SourceURL = request.SourceSummary() response.ProcessingTime = 0 response.Timestamp = time.Now() return response } // createDebugInfo 创建调试信息 func (ce *ConversionEngine) createDebugInfo( request *ConversionRequest, startTime, endTime time.Time, parseResult *ParseResult, generateResult *GenerateResult, ) map[string]interface{} { totalTime := endTime.Sub(startTime).Milliseconds() info := map[string]interface{}{ "parse_time": 0, "generate_time": 0, "total_time": totalTime, "input_size": 0, "output_size": len(generateResult.GeneratedOutput), "proxy_count": len(parseResult.ValidConfigs), "error_count": len(parseResult.ErrorMessages), "protocols": parseResult.ProtocolStats, "format_features": generateResult.FormatFeatures, } if generateResult.GenerateStats != nil { info["generate_stats"] = map[string]interface{}{ "generated_proxies": generateResult.GenerateStats.GeneratedProxies, "generated_groups": generateResult.GenerateStats.GeneratedGroups, "generated_rules": generateResult.GenerateStats.GeneratedRules, "features": generateResult.GenerateStats.FormatFeatures, } } return info } // ParseResult 解析结果 type ParseResult struct { ValidConfigs []*parser.ProxyConfig `json:"validConfigs"` InvalidInputs []string `json:"invalidInputs"` ErrorMessages []string `json:"errorMessages"` ProtocolStats map[string]int `json:"protocolStats"` } // GenerateResult 生成结果 type GenerateResult struct { GeneratedOutput string `json:"generatedOutput"` FormatFeatures map[string]bool `json:"formatFeatures"` GenerateStats *GenerateStats `json:"generateStats"` } func (ce *ConversionEngine) applyManagedConfig(request *ConversionRequest, content string) string { if strings.ToLower(request.Target) != "surge" { return content } if !request.Upload { return content } managedURL := ce.buildManagedConfigURL(request) if managedURL == "" { return content } header := fmt.Sprintf("#!MANAGED-CONFIG %s", managedURL) if interval := ce.normalizeInterval(request.Interval); interval != "" { header += " interval=" + interval } header += fmt.Sprintf(" strict=%t", request.Strict) return header + "\n\n" + content } func (ce *ConversionEngine) buildManagedConfigURL(request *ConversionRequest) string { params := url.Values{} params.Set("target", request.Target) if summary := request.SourceSummary(); summary != "" { params.Set("url", summary) } if request.Strict { params.Set("strict", "true") } if request.Interval != "" { params.Set("interval", request.Interval) } return "/sub?" + params.Encode() } func (ce *ConversionEngine) normalizeInterval(raw string) string { if raw == "" { return "" } value, err := strconv.Atoi(raw) if err != nil || value <= 0 { return "" } return strconv.Itoa(value) } func (ce *ConversionEngine) profileUpdateHeader(raw string) string { if raw == "" { return "" } value, err := strconv.Atoi(raw) if err != nil || value <= 0 { return "" } hours := value / 3600 if hours <= 0 { hours = 1 } return strconv.Itoa(hours) }