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 <noreply@anthropic.com>
This commit is contained in:
haimu0427 2026-03-12 15:14:57 +08:00
parent 9478e1a3e4
commit 9127fda70b
3 changed files with 58 additions and 20 deletions

View file

@ -91,12 +91,21 @@ func (s *Server) initRouter() (*gin.Engine, error) {
return nil, err return nil, err
} }
// Determine if JSON subscription endpoint is enabled ClashPath, err := s.settingService.GetSubClashPath()
if err != nil {
return nil, err
}
subJsonEnable, err := s.settingService.GetSubJsonEnable() subJsonEnable, err := s.settingService.GetSubJsonEnable()
if err != nil { if err != nil {
return nil, err return nil, err
} }
subClashEnable, err := s.settingService.GetSubClashEnable()
if err != nil {
return nil, err
}
// Set base_path based on LinksPath for template rendering // Set base_path based on LinksPath for template rendering
// Ensure LinksPath ends with "/" for proper asset URL generation // Ensure LinksPath ends with "/" for proper asset URL generation
basePath := LinksPath basePath := LinksPath
@ -255,7 +264,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
g := engine.Group("/") g := engine.Group("/")
s.sub = NewSUBController( s.sub = NewSUBController(
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates, g, LinksPath, JsonPath, ClashPath, subJsonEnable, subClashEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl, SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules) SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)

View file

@ -21,12 +21,15 @@ type SUBController struct {
subRoutingRules string subRoutingRules string
subPath string subPath string
subJsonPath string subJsonPath string
subClashPath string
jsonEnabled bool jsonEnabled bool
clashEnabled bool
subEncrypt bool subEncrypt bool
updateInterval string updateInterval string
subService *SubService subService *SubService
subJsonService *SubJsonService subJsonService *SubJsonService
subClashService *SubClashService
} }
// NewSUBController creates a new subscription controller with the given configuration. // NewSUBController creates a new subscription controller with the given configuration.
@ -34,7 +37,9 @@ func NewSUBController(
g *gin.RouterGroup, g *gin.RouterGroup,
subPath string, subPath string,
jsonPath string, jsonPath string,
clashPath string,
jsonEnabled bool, jsonEnabled bool,
clashEnabled bool,
encrypt bool, encrypt bool,
showInfo bool, showInfo bool,
rModel string, rModel string,
@ -60,12 +65,15 @@ func NewSUBController(
subRoutingRules: subRoutingRules, subRoutingRules: subRoutingRules,
subPath: subPath, subPath: subPath,
subJsonPath: jsonPath, subJsonPath: jsonPath,
subClashPath: clashPath,
jsonEnabled: jsonEnabled, jsonEnabled: jsonEnabled,
clashEnabled: clashEnabled,
subEncrypt: encrypt, subEncrypt: encrypt,
updateInterval: update, updateInterval: update,
subService: sub, subService: sub,
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
subClashService: NewSubClashService(sub),
} }
a.initRouter(g) a.initRouter(g)
return a return a
@ -80,6 +88,10 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
gJson := g.Group(a.subJsonPath) gJson := g.Group(a.subJsonPath)
gJson.GET(":subid", a.subJsons) 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. // 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") accept := c.GetHeader("Accept")
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") { if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
// Build page data in service // 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 { if !a.jsonEnabled {
subJsonURL = "" subJsonURL = ""
} }
if !a.clashEnabled {
subClashURL = ""
}
// Get base_path from context (set by middleware) // Get base_path from context (set by middleware)
basePath, exists := c.Get("base_path") basePath, exists := c.Get("base_path")
if !exists { if !exists {
@ -116,7 +131,7 @@ func (a *SUBController) subs(c *gin.Context) {
// Remove trailing slash if exists, add subId, then add trailing slash // Remove trailing slash if exists, add subId, then add trailing slash
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/" 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{ c.HTML(200, "subpage.html", gin.H{
"title": "subscription.title", "title": "subscription.title",
"cur_ver": config.GetVersion(), "cur_ver": config.GetVersion(),
@ -136,6 +151,7 @@ func (a *SUBController) subs(c *gin.Context) {
"totalByte": page.TotalByte, "totalByte": page.TotalByte,
"subUrl": page.SubUrl, "subUrl": page.SubUrl,
"subJsonUrl": page.SubJsonUrl, "subJsonUrl": page.SubJsonUrl,
"subClashUrl": page.SubClashUrl,
"result": page.Result, "result": page.Result,
}) })
return return
@ -165,7 +181,6 @@ func (a *SUBController) subJsons(c *gin.Context) {
if err != nil || len(jsonSub) == 0 { if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
// Add headers
profileUrl := a.subProfileUrl profileUrl := a.subProfileUrl
if profileUrl == "" { if profileUrl == "" {
profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI) 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. // ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
func (a *SUBController) ApplyCommonHeaders( func (a *SUBController) ApplyCommonHeaders(
c *gin.Context, c *gin.Context,

View file

@ -1031,6 +1031,7 @@ type PageData struct {
TotalByte int64 TotalByte int64
SubUrl string SubUrl string
SubJsonUrl string SubJsonUrl string
SubClashUrl string
Result []string Result []string
} }
@ -1080,29 +1081,25 @@ func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string,
// BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID. // BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID.
// It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components. // It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components.
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) { func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subClashPath, subId string) (subURL, subJsonURL, subClashURL string) {
// Input validation
if subId == "" { if subId == "" {
return "", "" return "", "", ""
} }
// Get configured URIs first (highest priority)
configuredSubURI, _ := s.settingService.GetSubURI() configuredSubURI, _ := s.settingService.GetSubURI()
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI() configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
configuredSubClashURI, _ := s.settingService.GetSubClashURI()
// Determine base scheme and host (cached to avoid duplicate calls)
var baseScheme, baseHostWithPort string var baseScheme, baseHostWithPort string
if configuredSubURI == "" || configuredSubJsonURI == "" { if configuredSubURI == "" || configuredSubJsonURI == "" || configuredSubClashURI == "" {
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort) baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
} }
// Build subscription URL
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId) subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
// Build JSON subscription URL
subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId) subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId)
subClashURL = s.buildSingleURL(configuredSubClashURI, baseScheme, baseHostWithPort, subClashPath, subId)
return subURL, subJsonURL return subURL, subJsonURL, subClashURL
} }
// getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values // getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values
@ -1149,7 +1146,7 @@ func (s *SubService) joinPathWithID(basePath, subId string) string {
// BuildPageData parses header and prepares the template view model. // BuildPageData parses header and prepares the template view model.
// BuildPageData constructs page data for rendering the subscription information page. // BuildPageData constructs page data for rendering the subscription information page.
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData { func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL, subClashURL string, basePath string) PageData {
download := common.FormatTraffic(traffic.Down) download := common.FormatTraffic(traffic.Down)
upload := common.FormatTraffic(traffic.Up) upload := common.FormatTraffic(traffic.Up)
total := "∞" total := "∞"
@ -1183,6 +1180,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
TotalByte: traffic.Total, TotalByte: traffic.Total,
SubUrl: subURL, SubUrl: subURL,
SubJsonUrl: subJsonURL, SubJsonUrl: subJsonURL,
SubClashUrl: subClashURL,
Result: subs, Result: subs,
} }
} }