Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorzhuzn <haimu0427@Outlook.com>2026-04-19 23:26:13 +0300
committerGitHub <noreply@github.com>2026-04-19 23:26:13 +0300
commitd580086361036f87af843d0f7386bdc54736720a (patch)
treef22944b1ab1ea8c29858d796cc248b7b16436427 /sub/subController.go
parent1e3b366fba3054698d55e36891d022a513e5a942 (diff)
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 <noreply@anthropic.com> * 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> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * fix --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
Diffstat (limited to 'sub/subController.go')
-rw-r--r--sub/subController.go45
1 files changed, 38 insertions, 7 deletions
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,