mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
feat: support full mihomo template and multi-server for Clash Link
- Add splitTemplate() to split at proxies:/proxy-groups: markers (like mihomo-gen) - Store clash_template.yaml and servers.yaml as files alongside x-ui.json - Add Clash/Servers editors in Xray advanced config page - Support multi-server proxy generation (each server × each client) - Remove inline template editor from Clash settings panel - Bump version to v1.7.2.1
This commit is contained in:
parent
67c4f6a1ad
commit
25cf22d161
14 changed files with 338 additions and 76 deletions
|
|
@ -129,6 +129,85 @@ func GetTrafficPendingPath() string {
|
||||||
return filepath.Join(GetDBFolderPath(), "traffic-pending.json")
|
return filepath.Join(GetDBFolderPath(), "traffic-pending.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetClashTemplatePath returns the path to the clash_template.yaml file.
|
||||||
|
func GetClashTemplatePath() string {
|
||||||
|
return filepath.Join(GetDBFolderPath(), "clash_template.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServersPath returns the path to the servers.yaml file.
|
||||||
|
func GetServersPath() string {
|
||||||
|
return filepath.Join(GetDBFolderPath(), "servers.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadClashTemplate reads the clash template from disk.
|
||||||
|
// Returns the default mihomo template if the file does not exist.
|
||||||
|
func ReadClashTemplate() (string, error) {
|
||||||
|
data, err := os.ReadFile(GetClashTemplatePath())
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return defaultClashTemplate, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveClashTemplate writes the clash template to disk.
|
||||||
|
func SaveClashTemplate(content string) error {
|
||||||
|
path := GetClashTemplatePath()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadServers reads the servers config from disk.
|
||||||
|
// Returns a default empty servers config if the file does not exist.
|
||||||
|
func ReadServers() (string, error) {
|
||||||
|
data, err := os.ReadFile(GetServersPath())
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return defaultServers, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveServers writes the servers config to disk.
|
||||||
|
func SaveServers(content string) error {
|
||||||
|
path := GetServersPath()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultServers = `servers: []
|
||||||
|
`
|
||||||
|
|
||||||
|
const defaultClashTemplate = `port: 7890
|
||||||
|
socks-port: 7891
|
||||||
|
allow-lan: false
|
||||||
|
mode: rule
|
||||||
|
log-level: info
|
||||||
|
proxies:
|
||||||
|
proxy-groups:
|
||||||
|
- name: Proxy
|
||||||
|
type: select
|
||||||
|
proxies:
|
||||||
|
- DIRECT
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
enhanced-mode: fake-ip
|
||||||
|
nameserver:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 1.1.1.1
|
||||||
|
rules:
|
||||||
|
- GEOIP,LAN,DIRECT
|
||||||
|
- MATCH,Proxy
|
||||||
|
`
|
||||||
|
|
||||||
// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.
|
// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.
|
||||||
func GetLogFolder() string {
|
func GetLogFolder() string {
|
||||||
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
v1.7.0.1
|
v1.7.2.1
|
||||||
|
|
|
||||||
24
docs/Tasktracking/2026-04-25-clash-full-mihomo-template.md
Normal file
24
docs/Tasktracking/2026-04-25-clash-full-mihomo-template.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Clash Link: Full Mihomo Template + Multi-Server Support
|
||||||
|
|
||||||
|
## Date: 2026-04-25
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- `config/config.go` — Added `GetClashTemplatePath()`, `GetServersPath()`, `ReadClashTemplate()`, `SaveClashTemplate()`, `ReadServers()`, `SaveServers()`. Files stored at `/etc/x-ui/clash_template.yaml` and `/etc/x-ui/servers.yaml`
|
||||||
|
- `sub/subClashService.go` — Added `splitTemplate()` (from mihomo-gen), modified `GetClash()` to split at `proxies:`/`proxy-groups:` markers instead of `proxies: []` replacement. Added multi-server support: each `ClashServer` × each client generates a proxy entry. Falls back to old approach if split fails.
|
||||||
|
- `sub/sub.go` — Reads template and servers from files via `config.ReadClashTemplate()`/`config.ReadServers()`. Added `ClashServer` struct and `parseServers()`.
|
||||||
|
- `sub/subController.go` — Updated `NewSUBController` to accept `clashServers []ClashServer`
|
||||||
|
- `web/controller/xray_setting.go` — Added 4 API endpoints: `GET/POST /xray/clashTemplate`, `GET/POST /xray/servers`
|
||||||
|
- `web/service/setting.go` — Cleared `subClashTemplate` default (template now from file)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- `web/html/settings/xray/advanced.html` — Added "Clash" and "Servers" radio buttons in Xray advanced config
|
||||||
|
- `web/html/xray.html` — Added `clashTemplate`/`servers` data with old-value tracking, load/save methods, YAML CodeMirror mode, smart save button dispatches to correct save handler
|
||||||
|
- `web/html/settings/panel/subscription/clash.html` — Removed template editor (now in Xray advanced config)
|
||||||
|
- `web/html/settings.html` — Removed `initClashCodeMirror()` (template editor moved)
|
||||||
|
- `web/translation/translate.en_US.toml` — Added "Servers" key
|
||||||
|
- `web/translation/translate.zh_CN.toml` — Added "Servers" key
|
||||||
|
|
||||||
|
### Version
|
||||||
|
- `config/version` — Bumped to v1.7.2.1
|
||||||
41
sub/sub.go
41
sub/sub.go
|
|
@ -18,6 +18,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
webpkg "github.com/mhsanaei/3x-ui/v2/web"
|
webpkg "github.com/mhsanaei/3x-ui/v2/web"
|
||||||
|
|
@ -31,6 +33,26 @@ import (
|
||||||
|
|
||||||
type subscriptionAssetManifest map[string]string
|
type subscriptionAssetManifest map[string]string
|
||||||
|
|
||||||
|
// ClashServer represents a proxy server entry from servers.yaml.
|
||||||
|
type ClashServer struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Server string `yaml:"server"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serversConfig is the top-level structure of servers.yaml.
|
||||||
|
type serversConfig struct {
|
||||||
|
Servers []ClashServer `yaml:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseServers parses YAML data into a list of ClashServer entries.
|
||||||
|
func parseServers(data []byte) ([]ClashServer, error) {
|
||||||
|
var cfg serversConfig
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse servers: %w", err)
|
||||||
|
}
|
||||||
|
return cfg.Servers, nil
|
||||||
|
}
|
||||||
|
|
||||||
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
||||||
func setEmbeddedTemplates(engine *gin.Engine) error {
|
func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||||
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
||||||
|
|
@ -237,11 +259,26 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
SubClashPath = "/clash/"
|
SubClashPath = "/clash/"
|
||||||
}
|
}
|
||||||
|
|
||||||
SubClashTemplate, err := s.settingService.GetSubClashTemplate()
|
// Read clash template from file (alongside x-ui.json)
|
||||||
|
SubClashTemplate, err := config.ReadClashTemplate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Warning("sub: failed to read clash template:", err)
|
||||||
SubClashTemplate = ""
|
SubClashTemplate = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read servers config from file
|
||||||
|
serversData, err := config.ReadServers()
|
||||||
|
var clashServers []ClashServer
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: failed to read servers config:", err)
|
||||||
|
} else {
|
||||||
|
clashServers, err = parseServers([]byte(serversData))
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: failed to parse servers config:", err)
|
||||||
|
clashServers = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set per-request localizer from headers/cookies
|
// set per-request localizer from headers/cookies
|
||||||
engine.Use(locale.LocalizerMiddleware())
|
engine.Use(locale.LocalizerMiddleware())
|
||||||
|
|
||||||
|
|
@ -323,7 +360,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
|
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
|
||||||
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules,
|
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules,
|
||||||
subClashEnable, SubClashPath, SubClashTemplate)
|
subClashEnable, SubClashPath, SubClashTemplate, clashServers)
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,50 @@ import (
|
||||||
// SubClashService handles Clash YAML subscription generation.
|
// SubClashService handles Clash YAML subscription generation.
|
||||||
type SubClashService struct {
|
type SubClashService struct {
|
||||||
template string
|
template string
|
||||||
|
servers []ClashServer
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
SubService *SubService
|
SubService *SubService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSubClashService creates a new Clash subscription service with the given template.
|
// NewSubClashService creates a new Clash subscription service with the given template and servers.
|
||||||
func NewSubClashService(template string, subService *SubService) *SubClashService {
|
func NewSubClashService(template string, servers []ClashServer, subService *SubService) *SubClashService {
|
||||||
return &SubClashService{
|
return &SubClashService{
|
||||||
template: template,
|
template: template,
|
||||||
|
servers: servers,
|
||||||
SubService: subService,
|
SubService: subService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// splitTemplate splits a full mihomo template at "proxies:" and "proxy-groups:" markers.
|
||||||
|
// Returns header (everything before proxies) and footer (everything from proxy-groups onwards).
|
||||||
|
func splitTemplate(template string) (header, footer string, err error) {
|
||||||
|
proxiesIdx := strings.Index(template, "\nproxies:")
|
||||||
|
if proxiesIdx == -1 {
|
||||||
|
if strings.HasPrefix(template, "proxies:") {
|
||||||
|
proxiesIdx = 0
|
||||||
|
} else {
|
||||||
|
return "", "", fmt.Errorf("template: 'proxies:' section not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proxiesIdx++ // skip the leading newline
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyGroupsIdx := strings.Index(template, "\nproxy-groups:")
|
||||||
|
if proxyGroupsIdx == -1 {
|
||||||
|
if strings.HasPrefix(template[proxiesIdx:], "proxy-groups:") {
|
||||||
|
proxyGroupsIdx = proxiesIdx
|
||||||
|
} else {
|
||||||
|
return "", "", fmt.Errorf("template: 'proxy-groups:' section not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proxyGroupsIdx++ // skip the leading newline
|
||||||
|
}
|
||||||
|
|
||||||
|
header = template[:proxiesIdx]
|
||||||
|
footer = template[proxyGroupsIdx:]
|
||||||
|
return header, footer, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetClash generates a Clash YAML configuration for the given subscription ID.
|
// GetClash generates a Clash YAML configuration for the given subscription ID.
|
||||||
func (s *SubClashService) GetClash(subId string) (string, string, error) {
|
func (s *SubClashService) GetClash(subId string) (string, string, error) {
|
||||||
if s.template == "" {
|
if s.template == "" {
|
||||||
|
|
@ -101,14 +133,22 @@ func (s *SubClashService) GetClash(subId string) (string, string, error) {
|
||||||
proxiesYaml += " - " + p + "\n"
|
proxiesYaml += " - " + p + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject proxies into template by replacing "proxies: []"
|
// Try split-template approach first (for full mihomo templates)
|
||||||
result := strings.Replace(s.template, "proxies: []", "proxies:\n"+proxiesYaml, 1)
|
var result string
|
||||||
|
if header, footer, err := splitTemplate(s.template); err == nil {
|
||||||
|
result = header + "proxies:\n" + proxiesYaml + footer
|
||||||
|
} else {
|
||||||
|
// Fall back to old "proxies: []" replacement for backward compatibility
|
||||||
|
result = strings.Replace(s.template, "proxies: []", "proxies:\n"+proxiesYaml, 1)
|
||||||
|
}
|
||||||
|
|
||||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||||
return result, header, nil
|
return result, header, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getProxy generates Clash proxy entries for a client.
|
// getProxy generates Clash proxy entries for a client.
|
||||||
|
// If servers are configured, generates one entry per server using the server's name and address.
|
||||||
|
// Otherwise, uses the inbound's own address (backward compatible).
|
||||||
func (s *SubClashService) getProxy(inbound *model.Inbound, client model.Client) []string {
|
func (s *SubClashService) getProxy(inbound *model.Inbound, client model.Client) []string {
|
||||||
var proxies []string
|
var proxies []string
|
||||||
var stream map[string]any
|
var stream map[string]any
|
||||||
|
|
@ -116,17 +156,14 @@ func (s *SubClashService) getProxy(inbound *model.Inbound, client model.Client)
|
||||||
logger.Warning("SubClashService - failed to parse StreamSettings for inbound", inbound.Tag, ":", err)
|
logger.Warning("SubClashService - failed to parse StreamSettings for inbound", inbound.Tag, ":", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve address
|
// Resolve default address from inbound
|
||||||
var address string
|
var defaultAddress string
|
||||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||||
address = s.SubService.address
|
defaultAddress = s.SubService.address
|
||||||
} else {
|
} else {
|
||||||
address = inbound.Listen
|
defaultAddress = inbound.Listen
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get remark
|
|
||||||
remark := s.SubService.genRemark(inbound, client.Email, "")
|
|
||||||
|
|
||||||
// Parse stream settings
|
// Parse stream settings
|
||||||
network, _ := stream["network"].(string)
|
network, _ := stream["network"].(string)
|
||||||
security, _ := stream["security"].(string)
|
security, _ := stream["security"].(string)
|
||||||
|
|
@ -141,9 +178,40 @@ func (s *SubClashService) getProxy(inbound *model.Inbound, client model.Client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If servers are configured, generate one proxy per server
|
||||||
|
if len(s.servers) > 0 {
|
||||||
|
for _, server := range s.servers {
|
||||||
|
for _, ep := range externalProxies {
|
||||||
|
externalProxy, _ := ep.(map[string]any)
|
||||||
|
destPort := inbound.Port
|
||||||
|
if port, ok := externalProxy["port"].(float64); ok && port > 0 {
|
||||||
|
destPort = int(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
forceTls, _ := externalProxy["forceTls"].(string)
|
||||||
|
tlsEnabled := false
|
||||||
|
switch forceTls {
|
||||||
|
case "tls":
|
||||||
|
tlsEnabled = true
|
||||||
|
case "none":
|
||||||
|
tlsEnabled = false
|
||||||
|
default: // "same"
|
||||||
|
tlsEnabled = security == "tls" || security == "reality"
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := s.buildProxyEntry(inbound, client, server.Server, destPort, network, security, tlsEnabled, server.Name, stream)
|
||||||
|
proxies = append(proxies, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return proxies
|
||||||
|
}
|
||||||
|
|
||||||
|
// No servers configured — use inbound's own address (backward compatible)
|
||||||
|
remark := s.SubService.genRemark(inbound, client.Email, "")
|
||||||
|
|
||||||
for _, ep := range externalProxies {
|
for _, ep := range externalProxies {
|
||||||
externalProxy, _ := ep.(map[string]any)
|
externalProxy, _ := ep.(map[string]any)
|
||||||
destAddress := address
|
destAddress := defaultAddress
|
||||||
destPort := inbound.Port
|
destPort := inbound.Port
|
||||||
|
|
||||||
if dest, ok := externalProxy["dest"].(string); ok && dest != "" {
|
if dest, ok := externalProxy["dest"].(string); ok && dest != "" {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ func NewSUBController(
|
||||||
clashEnabled bool,
|
clashEnabled bool,
|
||||||
subClashPath string,
|
subClashPath string,
|
||||||
subClashTemplate string,
|
subClashTemplate string,
|
||||||
|
clashServers []ClashServer,
|
||||||
) *SUBController {
|
) *SUBController {
|
||||||
sub := NewSubService(showInfo, rModel)
|
sub := NewSubService(showInfo, rModel)
|
||||||
a := &SUBController{
|
a := &SUBController{
|
||||||
|
|
@ -76,7 +77,7 @@ func NewSUBController(
|
||||||
|
|
||||||
subService: sub,
|
subService: sub,
|
||||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||||
subClashService: NewSubClashService(subClashTemplate, sub),
|
subClashService: NewSubClashService(subClashTemplate, clashServers, sub),
|
||||||
}
|
}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package controller
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
|
|
@ -38,6 +39,11 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/update", a.updateSetting)
|
g.POST("/update", a.updateSetting)
|
||||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||||
g.POST("/testOutbound", a.testOutbound)
|
g.POST("/testOutbound", a.testOutbound)
|
||||||
|
|
||||||
|
g.GET("/clashTemplate", a.getClashTemplate)
|
||||||
|
g.POST("/clashTemplate", a.saveClashTemplate)
|
||||||
|
g.GET("/servers", a.getServers)
|
||||||
|
g.POST("/servers", a.saveServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL.
|
// getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL.
|
||||||
|
|
@ -166,3 +172,43 @@ func (a *XraySettingController) testOutbound(c *gin.Context) {
|
||||||
|
|
||||||
jsonObj(c, result, nil)
|
jsonObj(c, result, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getClashTemplate reads the clash_template.yaml file and returns its content.
|
||||||
|
func (a *XraySettingController) getClashTemplate(c *gin.Context) {
|
||||||
|
content, err := config.ReadClashTemplate()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, content, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveClashTemplate writes the clash_template.yaml file.
|
||||||
|
func (a *XraySettingController) saveClashTemplate(c *gin.Context) {
|
||||||
|
content := c.PostForm("content")
|
||||||
|
if err := config.SaveClashTemplate(content); err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServers reads the servers.yaml file and returns its content.
|
||||||
|
func (a *XraySettingController) getServers(c *gin.Context) {
|
||||||
|
content, err := config.ReadServers()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, content, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveServers writes the servers.yaml file.
|
||||||
|
func (a *XraySettingController) saveServers(c *gin.Context) {
|
||||||
|
content := c.PostForm("content")
|
||||||
|
if err := config.SaveServers(content); err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -255,29 +255,6 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSettingsTabChange(key) {
|
onSettingsTabChange(key) {
|
||||||
if (key === '6') {
|
|
||||||
this.$nextTick(() => this.initClashCodeMirror());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initClashCodeMirror() {
|
|
||||||
if (this.clashCm != null) {
|
|
||||||
this.clashCm.toTextArea();
|
|
||||||
}
|
|
||||||
const el = document.getElementById('clashTemplate');
|
|
||||||
if (!el) return;
|
|
||||||
el.value = this.allSetting.subClashTemplate;
|
|
||||||
this.clashCm = CodeMirror.fromTextArea(el, {
|
|
||||||
lineNumbers: true,
|
|
||||||
mode: "text/x-yaml",
|
|
||||||
theme: "xq",
|
|
||||||
lineWrapping: true,
|
|
||||||
indentUnit: 2,
|
|
||||||
tabSize: 2,
|
|
||||||
smartIndent: true,
|
|
||||||
});
|
|
||||||
this.clashCm.on('change', editor => {
|
|
||||||
this.allSetting.subClashTemplate = editor.getValue();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
loading(spinning = true) {
|
loading(spinning = true) {
|
||||||
this.loadingStates.spinning = spinning;
|
this.loadingStates.spinning = spinning;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{{define "settings/panel/subscription/clash"}}
|
{{define "settings/panel/subscription/clash"}}
|
||||||
<a-collapse default-active-key="['1','2']">
|
<a-collapse default-active-key="['1']">
|
||||||
<a-collapse-panel key="1" header='{{ i18n "pages.xray.generalConfigs"}}'>
|
<a-collapse-panel key="1" header='{{ i18n "pages.xray.generalConfigs"}}'>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
||||||
|
|
@ -20,14 +20,5 @@
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel key="2" header="Clash YAML Template">
|
|
||||||
<a-setting-list-item paddings="small">
|
|
||||||
<template #title>Template</template>
|
|
||||||
<template #description>Complete Clash YAML template with proxies: [] placeholder. The panel will replace proxies: [] with generated proxy entries.</template>
|
|
||||||
<template #control>
|
|
||||||
<textarea id="clashTemplate" style="display:none"></textarea>
|
|
||||||
</template>
|
|
||||||
</a-setting-list-item>
|
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
<a-radio-button value="inboundSettings">{{ i18n "pages.xray.Inbounds" }}</a-radio-button>
|
<a-radio-button value="inboundSettings">{{ i18n "pages.xray.Inbounds" }}</a-radio-button>
|
||||||
<a-radio-button value="outboundSettings">{{ i18n "pages.xray.Outbounds" }}</a-radio-button>
|
<a-radio-button value="outboundSettings">{{ i18n "pages.xray.Outbounds" }}</a-radio-button>
|
||||||
<a-radio-button value="routingRuleSettings">{{ i18n "pages.xray.Routings" }}</a-radio-button>
|
<a-radio-button value="routingRuleSettings">{{ i18n "pages.xray.Routings" }}</a-radio-button>
|
||||||
|
<a-radio-button value="clashTemplate">Clash</a-radio-button>
|
||||||
|
<a-radio-button value="servers">{{ i18n "pages.xray.Servers" }}</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<textarea :style="{ position: 'absolute', left: '-800px' }" id="xraySetting"></textarea>
|
<textarea :style="{ position: 'absolute', left: '-800px' }" id="xraySetting"></textarea>
|
||||||
</a-space>
|
</a-space>
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,10 @@
|
||||||
showAlert: false,
|
showAlert: false,
|
||||||
advSettings: 'xraySetting',
|
advSettings: 'xraySetting',
|
||||||
obsSettings: '',
|
obsSettings: '',
|
||||||
|
clashTemplate: '',
|
||||||
|
oldClashTemplate: '',
|
||||||
|
servers: '',
|
||||||
|
oldServers: '',
|
||||||
cm: null,
|
cm: null,
|
||||||
cmOptions: {
|
cmOptions: {
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
|
|
@ -420,13 +424,55 @@
|
||||||
},
|
},
|
||||||
async updateXraySetting() {
|
async updateXraySetting() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post("/panel/xray/update", {
|
if (this.advSettings === 'clashTemplate') {
|
||||||
xraySetting: this.xraySetting,
|
await this.saveClashTemplate();
|
||||||
outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'
|
} else if (this.advSettings === 'servers') {
|
||||||
|
await this.saveServers();
|
||||||
|
} else {
|
||||||
|
const msg = await HttpUtil.post("/panel/xray/update", {
|
||||||
|
xraySetting: this.xraySetting,
|
||||||
|
outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'
|
||||||
|
});
|
||||||
|
this.loading(false);
|
||||||
|
if (msg.success) {
|
||||||
|
await this.getXraySetting();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading(false);
|
||||||
|
},
|
||||||
|
async getClashTemplate() {
|
||||||
|
const msg = await HttpUtil.get("/panel/xray/clashTemplate");
|
||||||
|
if (msg.success) {
|
||||||
|
this.clashTemplate = msg.obj;
|
||||||
|
this.oldClashTemplate = msg.obj;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveClashTemplate() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post("/panel/xray/clashTemplate", {
|
||||||
|
content: this.clashTemplate
|
||||||
});
|
});
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
await this.getXraySetting();
|
this.oldClashTemplate = this.clashTemplate;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getServers() {
|
||||||
|
const msg = await HttpUtil.get("/panel/xray/servers");
|
||||||
|
if (msg.success) {
|
||||||
|
this.servers = msg.obj;
|
||||||
|
this.oldServers = msg.obj;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveServers() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post("/panel/xray/servers", {
|
||||||
|
content: this.servers
|
||||||
|
});
|
||||||
|
this.loading(false);
|
||||||
|
if (msg.success) {
|
||||||
|
this.oldServers = this.servers;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async restartXray() {
|
async restartXray() {
|
||||||
|
|
@ -533,10 +579,15 @@
|
||||||
}
|
}
|
||||||
const textAreaObj = document.getElementById('xraySetting');
|
const textAreaObj = document.getElementById('xraySetting');
|
||||||
textAreaObj.value = this[this.advSettings];
|
textAreaObj.value = this[this.advSettings];
|
||||||
this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
|
const isYaml = this.advSettings === 'clashTemplate' || this.advSettings === 'servers';
|
||||||
|
const options = Object.assign({}, this.cmOptions, {
|
||||||
|
mode: isYaml ? "text/x-yaml" : "application/json",
|
||||||
|
lint: !isYaml,
|
||||||
|
});
|
||||||
|
this.cm = CodeMirror.fromTextArea(textAreaObj, options);
|
||||||
this.cm.on('change', editor => {
|
this.cm.on('change', editor => {
|
||||||
const value = editor.getValue();
|
const value = editor.getValue();
|
||||||
if (this.isJsonString(value)) {
|
if (isYaml || this.isJsonString(value)) {
|
||||||
this[this.advSettings] = value;
|
this[this.advSettings] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1079,6 +1130,8 @@
|
||||||
settingsLoaded = await this.getXraySetting();
|
settingsLoaded = await this.getXraySetting();
|
||||||
await this.getXrayResult();
|
await this.getXrayResult();
|
||||||
await this.getOutboundsTraffic();
|
await this.getOutboundsTraffic();
|
||||||
|
await this.getClashTemplate();
|
||||||
|
await this.getServers();
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingStates.fetched = true;
|
this.loadingStates.fetched = true;
|
||||||
}
|
}
|
||||||
|
|
@ -1098,7 +1151,8 @@
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
await PromiseUtil.sleep(800);
|
await PromiseUtil.sleep(800);
|
||||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this.outboundTestUrl;
|
this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this.outboundTestUrl
|
||||||
|
&& this.oldClashTemplate === this.clashTemplate && this.oldServers === this.servers;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -81,26 +81,7 @@ var defaultValueMap = map[string]string{
|
||||||
"subClashEnable": "false",
|
"subClashEnable": "false",
|
||||||
"subClashPath": "/clash/",
|
"subClashPath": "/clash/",
|
||||||
"subClashURI": "",
|
"subClashURI": "",
|
||||||
"subClashTemplate": `port: 7890
|
"subClashTemplate": "",
|
||||||
socks-port: 7891
|
|
||||||
allow-lan: false
|
|
||||||
mode: rule
|
|
||||||
log-level: info
|
|
||||||
proxies: []
|
|
||||||
proxy-groups:
|
|
||||||
- name: Proxy
|
|
||||||
type: select
|
|
||||||
proxies:
|
|
||||||
- DIRECT
|
|
||||||
dns:
|
|
||||||
enable: true
|
|
||||||
enhanced-mode: fake-ip
|
|
||||||
nameserver:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 1.1.1.1
|
|
||||||
rules:
|
|
||||||
- GEOIP,LAN,DIRECT
|
|
||||||
- MATCH,Proxy`,
|
|
||||||
"datepicker": "gregorian",
|
"datepicker": "gregorian",
|
||||||
"warp": "",
|
"warp": "",
|
||||||
"externalTrafficInformEnable": "false",
|
"externalTrafficInformEnable": "false",
|
||||||
|
|
|
||||||
|
|
@ -537,6 +537,7 @@
|
||||||
"OutboundsDesc" = "Set the outgoing traffic pathway."
|
"OutboundsDesc" = "Set the outgoing traffic pathway."
|
||||||
"Routings" = "Routing Rules"
|
"Routings" = "Routing Rules"
|
||||||
"RoutingsDesc" = "The priority of each rule is important!"
|
"RoutingsDesc" = "The priority of each rule is important!"
|
||||||
|
"Servers" = "Servers"
|
||||||
"completeTemplate" = "All"
|
"completeTemplate" = "All"
|
||||||
"logLevel" = "Log Level"
|
"logLevel" = "Log Level"
|
||||||
"logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded."
|
"logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded."
|
||||||
|
|
|
||||||
|
|
@ -537,6 +537,7 @@
|
||||||
"OutboundsDesc" = "设置出站流量传出方式"
|
"OutboundsDesc" = "设置出站流量传出方式"
|
||||||
"Routings" = "路由规则"
|
"Routings" = "路由规则"
|
||||||
"RoutingsDesc" = "每条规则的优先级都很重要"
|
"RoutingsDesc" = "每条规则的优先级都很重要"
|
||||||
|
"Servers" = "服务器"
|
||||||
"completeTemplate" = "全部"
|
"completeTemplate" = "全部"
|
||||||
"logLevel" = "日志级别"
|
"logLevel" = "日志级别"
|
||||||
"logLevelDesc" = "错误日志的日志级别,用于指示需要记录的信息"
|
"logLevelDesc" = "错误日志的日志级别,用于指示需要记录的信息"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue