From d580086361036f87af843d0f7386bdc54736720a Mon Sep 17 00:00:00 2001 From: zhuzn Date: Mon, 20 Apr 2026 04:26:13 +0800 Subject: feat add clash yaml convert (#3916) * docs(agents): add AI agent guidance documentation * feat(sub): add Clash/Mihomo YAML subscription service Add SubClashService to convert subscription links to Clash/Mihomo YAML format for direct client compatibility. Co-Authored-By: Claude Sonnet 4.6 * feat(sub): integrate Clash YAML endpoint into subscription system - Add Clash route handler in SUBController - Update BuildURLs to include Clash URL - Pass Clash settings through subscription pipeline Co-Authored-By: Claude Sonnet 4.6 * feat(web): add Clash settings to entity and service - Add SubClashEnable, SubClashPath, SubClashURI fields - Add getter methods for Clash configuration - Set default Clash path to /clash/ and enable by default Co-Authored-By: Claude Sonnet 4.6 * feat(ui): add Clash settings to subscription panels - Add Clash enable switch in general subscription settings - Add Clash path/URI configuration in formats panel - Display Clash QR code on subscription page - Rename JSON tab to "Formats" for clarity Co-Authored-By: Claude Sonnet 4.6 * feat(js): add Clash support to frontend models - Add subClashEnable, subClashPath, subClashURI to AllSetting - Generate and display Clash QR code on subscription page - Handle Clash URL in subscription data binding Co-Authored-By: Claude Sonnet 4.6 * fix --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Sanaei --- sub/subController.go | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) (limited to 'sub/subController.go') diff --git a/sub/subController.go b/sub/subController.go index 79ea755d..0e9e2c97 100644 --- a/sub/subController.go +++ b/sub/subController.go @@ -21,12 +21,15 @@ type SUBController struct { subRoutingRules string subPath string subJsonPath string + subClashPath string jsonEnabled bool + clashEnabled bool subEncrypt bool updateInterval string - subService *SubService - subJsonService *SubJsonService + subService *SubService + subJsonService *SubJsonService + subClashService *SubClashService } // NewSUBController creates a new subscription controller with the given configuration. @@ -34,7 +37,9 @@ func NewSUBController( g *gin.RouterGroup, subPath string, jsonPath string, + clashPath string, jsonEnabled bool, + clashEnabled bool, encrypt bool, showInfo bool, rModel string, @@ -60,12 +65,15 @@ func NewSUBController( subRoutingRules: subRoutingRules, subPath: subPath, subJsonPath: jsonPath, + subClashPath: clashPath, jsonEnabled: jsonEnabled, + clashEnabled: clashEnabled, subEncrypt: encrypt, updateInterval: update, - subService: sub, - subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), + subService: sub, + subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), + subClashService: NewSubClashService(sub), } a.initRouter(g) return a @@ -80,6 +88,10 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) { gJson := g.Group(a.subJsonPath) gJson.GET(":subid", a.subJsons) } + if a.clashEnabled { + gClash := g.Group(a.subClashPath) + gClash.GET(":subid", a.subClashs) + } } // subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data. @@ -99,10 +111,13 @@ func (a *SUBController) subs(c *gin.Context) { accept := c.GetHeader("Accept") if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") { // Build page data in service - subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId) + subURL, subJsonURL, subClashURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, a.subClashPath, subId) if !a.jsonEnabled { subJsonURL = "" } + if !a.clashEnabled { + subClashURL = "" + } // Get base_path from context (set by middleware) basePath, exists := c.Get("base_path") if !exists { @@ -116,7 +131,7 @@ func (a *SUBController) subs(c *gin.Context) { // Remove trailing slash if exists, add subId, then add trailing slash basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/" } - page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr) + page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, subClashURL, basePathStr) c.HTML(200, "subpage.html", gin.H{ "title": "subscription.title", "cur_ver": config.GetVersion(), @@ -136,6 +151,7 @@ func (a *SUBController) subs(c *gin.Context) { "totalByte": page.TotalByte, "subUrl": page.SubUrl, "subJsonUrl": page.SubJsonUrl, + "subClashUrl": page.SubClashUrl, "result": page.Result, }) return @@ -165,7 +181,6 @@ func (a *SUBController) subJsons(c *gin.Context) { if err != nil || len(jsonSub) == 0 { c.String(400, "Error!") } else { - // Add headers profileUrl := a.subProfileUrl if profileUrl == "" { profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI) @@ -176,6 +191,22 @@ func (a *SUBController) subJsons(c *gin.Context) { } } +func (a *SUBController) subClashs(c *gin.Context) { + subId := c.Param("subid") + scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c) + clashSub, header, err := a.subClashService.GetClash(subId, host) + if err != nil || len(clashSub) == 0 { + c.String(400, "Error!") + } else { + profileUrl := a.subProfileUrl + if profileUrl == "" { + profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI) + } + a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules) + c.Data(200, "application/yaml; charset=utf-8", []byte(clashSub)) + } +} + // ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title. func (a *SUBController) ApplyCommonHeaders( c *gin.Context, -- cgit v1.2.3