Files
SingBox-Gopanel/internal/protocol/clash.go
CN-JS-HuiBai e8e7664aff
All checks were successful
build / build (api, amd64, linux) (push) Successful in -15s
build / build (api, arm64, linux) (push) Successful in -29s
build / build (api.exe, amd64, windows) (push) Successful in -27s
软件功能基本开发完成,内测BUG等待修复
2026-04-17 22:31:09 +08:00

210 lines
5.0 KiB
Go

package protocol
import (
"fmt"
"strings"
"xboard-go/internal/model"
"xboard-go/internal/service"
"github.com/goccy/go-yaml"
)
func GenerateClash(servers []model.Server, user model.User) (string, error) {
return GenerateClashWithTemplate("clash", servers, user)
}
func GenerateClashWithTemplate(templateName string, servers []model.Server, user model.User) (string, error) {
template := strings.TrimSpace(service.GetSubscribeTemplate(templateName))
if template == "" {
return generateClashFallback(servers, user), nil
}
var config map[string]any
if err := yaml.Unmarshal([]byte(template), &config); err != nil {
return generateClashFallback(servers, user), nil
}
proxies := make([]any, 0, len(servers))
proxyNames := make([]string, 0, len(servers))
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
proxy := buildClashProxy(conf, user)
if proxy == nil {
continue
}
proxies = append(proxies, proxy)
proxyNames = append(proxyNames, conf.Name)
}
config["proxies"] = append(anySlice(config["proxies"]), proxies...)
config["proxy-groups"] = mergeClashProxyGroups(anySlice(config["proxy-groups"]), proxyNames)
if _, ok := config["rules"]; !ok {
config["rules"] = []any{"MATCH,Proxy"}
}
data, err := yaml.Marshal(config)
if err != nil {
return "", err
}
output := strings.ReplaceAll(string(data), "$app_name", service.MustGetString("app_name", "XBoard"))
return output, nil
}
func buildClashProxy(conf service.NodeServerConfig, user model.User) map[string]any {
switch conf.Protocol {
case "shadowsocks":
cipher, _ := conf.Cipher.(string)
return map[string]any{
"name": conf.Name,
"type": "ss",
"server": conf.RawHost,
"port": conf.ServerPort,
"cipher": cipher,
"password": user.UUID,
}
case "vmess":
return map[string]any{
"name": conf.Name,
"type": "vmess",
"server": conf.RawHost,
"port": conf.ServerPort,
"uuid": user.UUID,
"alterId": 0,
"cipher": "auto",
"udp": true,
}
case "trojan":
return map[string]any{
"name": conf.Name,
"type": "trojan",
"server": conf.RawHost,
"port": conf.ServerPort,
"password": user.UUID,
"udp": true,
}
default:
return nil
}
}
func mergeClashProxyGroups(groups []any, proxyNames []string) []any {
if len(groups) == 0 {
return []any{
map[string]any{
"name": "Proxy",
"type": "select",
"proxies": append([]any{"DIRECT"}, stringsToAny(proxyNames)...),
},
}
}
for index, item := range groups {
group, ok := item.(map[string]any)
if !ok {
continue
}
group["proxies"] = appendUniqueAny(anySlice(group["proxies"]), stringsToAny(proxyNames)...)
groups[index] = group
}
return groups
}
func anySlice(value any) []any {
switch typed := value.(type) {
case nil:
return []any{}
case []any:
return append([]any{}, typed...)
case []string:
result := make([]any, 0, len(typed))
for _, item := range typed {
result = append(result, item)
}
return result
default:
return []any{}
}
}
func stringsToAny(values []string) []any {
result := make([]any, 0, len(values))
for _, value := range values {
result = append(result, value)
}
return result
}
func appendUniqueAny(base []any, values ...any) []any {
existing := make(map[string]struct{}, len(base))
for _, item := range base {
existing[fmt.Sprint(item)] = struct{}{}
}
for _, item := range values {
key := fmt.Sprint(item)
if _, ok := existing[key]; ok {
continue
}
base = append(base, item)
existing[key] = struct{}{}
}
return base
}
func generateClashFallback(servers []model.Server, user model.User) string {
var builder strings.Builder
builder.WriteString("proxies:\n")
var proxyNames []string
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
proxy := buildClashProxy(conf, user)
if proxy == nil {
continue
}
switch proxy["type"] {
case "ss":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: ss\n server: %s\n port: %d\n cipher: %s\n password: %s\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
fmt.Sprint(proxy["cipher"]),
user.UUID,
))
case "vmess":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: vmess\n server: %s\n port: %d\n uuid: %s\n alterId: 0\n cipher: auto\n udp: true\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
user.UUID,
))
case "trojan":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: trojan\n server: %s\n port: %d\n password: %s\n udp: true\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
user.UUID,
))
}
proxyNames = append(proxyNames, fmt.Sprintf("\"%s\"", conf.Name))
}
builder.WriteString("\nproxy-groups:\n")
builder.WriteString(" - name: Proxy\n type: select\n proxies:\n - DIRECT\n")
for _, name := range proxyNames {
builder.WriteString(" - " + name + "\n")
}
builder.WriteString("\nrules:\n")
builder.WriteString(" - MATCH,Proxy\n")
return builder.String()
}