package model // SubscriptionTarget constants. const ( TargetMihomo = "mihomo" TargetStash = "stash" TargetSurge = "surge" TargetSurgeMac = "surge-mac" TargetSurfboard = "surfboard" TargetLoon = "loon" TargetEgern = "egern" TargetShadowrocket = "shadowrocket" TargetQX = "qx" TargetSingBox = "sing-box" TargetV2ray = "v2ray" TargetURI = "uri" TargetJSON = "json" ) // FrontendVersion per review-resolution #39. const FrontendVersion = "1.0.0" // TestURL per review-resolution #40. const TestURL = "https://www.gstatic.com/generate_204" // targetAliases maps alias strings to canonical target names. var targetAliases = map[string]string{ "clash": TargetMihomo, "clashmeta": TargetMihomo, "clash-meta": TargetMihomo, "meta": TargetMihomo, "mihomo": TargetMihomo, "stash": TargetStash, "surge": TargetSurge, "surgemac": TargetSurgeMac, "surge-mac": TargetSurgeMac, "surge_mac": TargetSurgeMac, "surfboard": TargetSurfboard, "loon": TargetLoon, "egern": TargetEgern, "shadowrocket": TargetShadowrocket, "sr": TargetShadowrocket, "qx": TargetQX, "quanx": TargetQX, "quantumult": TargetQX, "quantumultx": TargetQX, "quantumult-x": TargetQX, "singbox": TargetSingBox, "sing-box": TargetSingBox, "sfa": TargetSingBox, "karing": TargetSingBox, "v2ray": TargetV2ray, "v2rayn": TargetV2ray, "v2rayng": TargetV2ray, "base64": TargetV2ray, "uri": TargetURI, "uris": TargetURI, "plain": TargetURI, "text": TargetURI, "json": TargetJSON, "raw": TargetJSON, } // NormalizeTargetAlias returns the canonical target for an alias, or "" if unknown. func NormalizeTargetAlias(input any) string { if input == nil { return "" } s := toString(input) if s == "" { return "" } if t, ok := targetAliases[s]; ok { return t } return "" } // NormalizeTarget resolves a target from explicit input or User-Agent inference. // Per review-resolution #7 — full UA keyword mapping with priority order. func NormalizeTarget(target, ua string) string { v := toLowerString(target) if t, ok := targetAliases[v]; ok { return t } u := toLowerString(ua) if contains(u, "sing-box") || contains(u, "singbox") { return TargetSingBox } if contains(u, "v2ray") { return TargetV2ray } if contains(u, "surge") { if contains(u, "mac") { return TargetSurgeMac } return TargetSurge } if contains(u, "loon") { return TargetLoon } if contains(u, "egern") { return TargetEgern } if contains(u, "shadowrocket") { return TargetShadowrocket } if contains(u, "quantumult") { return TargetQX } if contains(u, "stash") { return TargetStash } return TargetMihomo } // IsTargetCompatible checks whether a proxy type can be rendered for the given target. func IsTargetCompatible(proxyType, target string) bool { if target == TargetMihomo || target == TargetStash || target == TargetJSON { return true } commonUri := []string{"ss", "ssr", "vmess", "vless", "trojan", "hysteria", "hysteria2", "tuic", "anytls", "http", "socks5", "wireguard"} switch target { case TargetURI, TargetV2ray, TargetShadowrocket: return containsAny(proxyType, commonUri) case TargetSingBox: return containsAny(proxyType, []string{"ss", "vmess", "vless", "trojan", "hysteria", "hysteria2", "tuic", "anytls", "http", "socks5", "wireguard"}) case TargetSurge: return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "snell"}) case TargetSurgeMac: return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "snell", "ssh", "h2-connect"}) case TargetSurfboard: return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5"}) case TargetLoon: return containsAny(proxyType, []string{"ss", "ssr", "vmess", "vless", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls", "wireguard"}) case TargetQX: return containsAny(proxyType, []string{"ss", "ssr", "vmess", "vless", "trojan", "http", "socks5", "anytls"}) case TargetEgern: return containsAny(proxyType, []string{"ss", "vmess", "trojan", "http", "socks5", "hysteria2", "tuic", "anytls"}) } return false } // GetTargetContentType returns the Content-Type for a target. func GetTargetContentType(target string) string { switch target { case TargetSingBox, TargetJSON: return "application/json; charset=utf-8" case TargetV2ray, TargetURI, TargetSurge, TargetSurgeMac, TargetSurfboard, TargetLoon, TargetShadowrocket, TargetQX: return "text/plain; charset=utf-8" default: return "text/yaml; charset=utf-8" } } // --- helpers --- func toString(v any) string { if s, ok := v.(string); ok { return s } return "" } func toLowerString(v any) string { s := toString(v) b := make([]byte, len(s)) for i := 0; i < len(s); i++ { c := s[i] if c >= 'A' && c <= 'Z' { c += 32 } b[i] = c } return string(b) } func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || indexOf(s, substr) >= 0) } func indexOf(s, substr string) int { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return i } } return -1 } func containsAny(s string, list []string) bool { for _, item := range list { if s == item { return true } } return false }