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
}
// Determine if JSON subscription endpoint is enabled
ClashPath, err := s.settingService.GetSubClashPath()
if err != nil {
return nil, err
}
subJsonEnable, err := s.settingService.GetSubJsonEnable()
if err != nil {
return nil, err
}
subClashEnable, err := s.settingService.GetSubClashEnable()
if err != nil {
return nil, err
}
// Set base_path based on LinksPath for template rendering
// Ensure LinksPath ends with "/" for proper asset URL generation
basePath := LinksPath
@ -255,7 +264,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
g := engine.Group("/")
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,
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)

View file

@ -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,

View file

@ -1031,6 +1031,7 @@ type PageData struct {
TotalByte int64
SubUrl string
SubJsonUrl string
SubClashUrl 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.
// 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) {
// Input validation
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subClashPath, subId string) (subURL, subJsonURL, subClashURL string) {
if subId == "" {
return "", ""
return "", "", ""
}
// Get configured URIs first (highest priority)
configuredSubURI, _ := s.settingService.GetSubURI()
configuredSubJsonURI, _ := s.settingService.GetSubJsonURI()
configuredSubClashURI, _ := s.settingService.GetSubClashURI()
// Determine base scheme and host (cached to avoid duplicate calls)
var baseScheme, baseHostWithPort string
if configuredSubURI == "" || configuredSubJsonURI == "" {
if configuredSubURI == "" || configuredSubJsonURI == "" || configuredSubClashURI == "" {
baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort)
}
// Build subscription URL
subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId)
// Build JSON subscription URL
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
@ -1149,7 +1146,7 @@ func (s *SubService) joinPathWithID(basePath, subId string) string {
// BuildPageData parses header and prepares the template view model.
// 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)
upload := common.FormatTraffic(traffic.Up)
total := "∞"
@ -1183,6 +1180,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
TotalByte: traffic.Total,
SubUrl: subURL,
SubJsonUrl: subJsonURL,
SubClashUrl: subClashURL,
Result: subs,
}
}