chore: sub-store Go 重写项目初始化

This commit is contained in:
2026-07-27 14:38:13 +08:00
commit 90e53aa754
85 changed files with 15271 additions and 0 deletions
+356
View File
@@ -0,0 +1,356 @@
package render
import (
"encoding/json"
"fmt"
"github.com/peterqiu0516/sub-store/internal/model"
"github.com/peterqiu0516/sub-store/internal/util"
)
// RenderSingBoxJson renders proxies as a sing-box JSON config.
// Per review-resolution #42: full structure with log, inbounds, outbounds, route.
func RenderSingBoxJson(proxies []model.ProxyNode) string {
var nodeOutbounds []map[string]any
for _, p := range proxies {
if out := ToSingBoxOutbound(p); out != nil {
nodeOutbounds = append(nodeOutbounds, out)
}
}
tags := make([]string, 0, len(nodeOutbounds))
for _, out := range nodeOutbounds {
if t, ok := out["tag"].(string); ok {
tags = append(tags, t)
}
}
if len(tags) == 0 {
// return minimal config even with no supported nodes
}
// Build outbounds: PROXY (selector), AUTO (urltest), node outbounds, DIRECT, REJECT
proxyOutbounds := append([]string{"AUTO"}, tags...)
outbounds := []any{
map[string]any{
"type": "selector",
"tag": "PROXY",
"outbounds": proxyOutbounds,
"default": "AUTO",
"interrupt_exist_connections": false,
},
map[string]any{
"type": "urltest",
"tag": "AUTO",
"outbounds": tags,
"url": model.TestURL,
"interval": "5m",
"tolerance": 50,
"interrupt_exist_connections": false,
},
}
for _, out := range nodeOutbounds {
outbounds = append(outbounds, out)
}
outbounds = append(outbounds,
map[string]any{"type": "direct", "tag": "DIRECT"},
map[string]any{"type": "block", "tag": "REJECT"},
)
doc := map[string]any{
"log": map[string]any{"level": "info"},
"inbounds": []any{
map[string]any{
"type": "mixed",
"tag": "mixed-in",
"listen": "0.0.0.0",
"listen_port": 7890,
"sniff": true,
},
},
"outbounds": outbounds,
"route": map[string]any{
"auto_detect_interface": true,
"final": "PROXY",
},
}
data, _ := json.MarshalIndent(doc, "", " ")
return string(data)
}
// ToSingBoxOutbound converts a proxy node to a sing-box outbound map.
// Returns nil for unsupported proxy types.
func ToSingBoxOutbound(proxy model.ProxyNode) map[string]any {
ptype := getString(proxy, "type")
server := getString(proxy, "server")
port := getInt(proxy, "port")
name := getString(proxy, "name")
switch ptype {
case "vless":
realityOpts, _ := proxy["reality-opts"].(map[string]any)
var tls any
if getBool(proxy, "tls") {
tlsMap := map[string]any{
"enabled": true,
"server_name": proxy["servername"],
"utls": map[string]any{"enabled": true, "fingerprint": strOr(proxy, "client-fingerprint", "chrome")},
}
if realityOpts != nil {
tlsMap["reality"] = util.StripUndefined(map[string]any{
"enabled": true,
"public_key": realityOpts["public-key"],
"short_id": realityOpts["short-id"],
})
}
tls = tlsMap
}
return util.StripUndefined(map[string]any{
"type": "vless",
"tag": name,
"server": server,
"server_port": port,
"uuid": proxy["uuid"],
"flow": proxy["flow"],
"network": strOr(proxy, "network", "tcp"),
"packet_encoding": "xudp",
"tls": tls,
})
case "hysteria2":
var obfs any
if o := proxy["obfs"]; o != nil {
obfs = map[string]any{"type": o, "password": proxy["obfs-password"]}
}
return util.StripUndefined(map[string]any{
"type": "hysteria2",
"tag": name,
"server": server,
"server_port": port,
"password": proxy["password"],
"obfs": obfs,
"tls": map[string]any{
"enabled": true,
"server_name": proxy["sni"],
"insecure": getBool(proxy, "skip-cert-verify"),
},
})
case "hysteria":
return util.StripUndefined(map[string]any{
"type": "hysteria",
"tag": name,
"server": server,
"server_port": port,
"auth_str": proxy["auth_str"],
"up_mbps": numberOrUndefined(proxy["up"]),
"down_mbps": numberOrUndefined(proxy["down"]),
"obfs": stringSetting(proxy["obfs"]),
"tls": map[string]any{
"enabled": true,
"server_name": proxy["sni"],
"insecure": getBool(proxy, "skip-cert-verify"),
},
})
case "anytls":
return util.StripUndefined(map[string]any{
"type": "anytls",
"tag": name,
"server": server,
"server_port": port,
"password": proxy["password"],
"tls": map[string]any{
"enabled": true,
"server_name": sniOr(proxy, "sni", "servername"),
"insecure": getBool(proxy, "skip-cert-verify"),
"utls": map[string]any{"enabled": true, "fingerprint": strOr(proxy, "client-fingerprint", "chrome")},
},
})
case "tuic":
return util.StripUndefined(map[string]any{
"type": "tuic",
"tag": name,
"server": server,
"server_port": port,
"uuid": proxy["uuid"],
"password": proxy["password"],
"congestion_control": proxy["congestion-controller"],
"udp_relay_mode": proxy["udp-relay-mode"],
"zero_rtt_handshake": proxy["reduce-rtt"],
"tls": map[string]any{
"enabled": true,
"server_name": proxy["sni"],
"insecure": getBool(proxy, "skip-cert-verify"),
},
})
case "trojan":
return util.StripUndefined(map[string]any{
"type": "trojan",
"tag": name,
"server": server,
"server_port": port,
"password": proxy["password"],
"tls": map[string]any{
"enabled": true,
"server_name": proxy["sni"],
"insecure": getBool(proxy, "skip-cert-verify"),
},
})
case "socks5":
var tls any
if getBool(proxy, "tls") {
tls = map[string]any{"enabled": true}
}
return util.StripUndefined(map[string]any{
"type": "socks",
"tag": name,
"server": server,
"server_port": port,
"version": "5",
"username": proxy["username"],
"password": proxy["password"],
"tls": tls,
})
case "http":
var tls any
if getBool(proxy, "tls") {
tls = map[string]any{"enabled": true}
}
return util.StripUndefined(map[string]any{
"type": "http",
"tag": name,
"server": server,
"server_port": port,
"username": proxy["username"],
"password": proxy["password"],
"tls": tls,
})
case "ss":
return util.StripUndefined(map[string]any{
"type": "shadowsocks",
"tag": name,
"server": server,
"server_port": port,
"method": proxy["cipher"],
"password": proxy["password"],
})
case "wireguard":
var localAddress []string
if ip := stringSetting(proxy["ip"]); ip != "" {
localAddress = append(localAddress, ip)
}
if ipv6 := stringSetting(proxy["ipv6"]); ipv6 != "" {
localAddress = append(localAddress, ipv6)
}
result := util.StripUndefined(map[string]any{
"type": "wireguard",
"tag": name,
"server": server,
"server_port": port,
"private_key": proxy["private-key"],
"peer_public_key": proxy["public-key"],
"pre_shared_key": proxy["pre-shared-key"],
})
if len(localAddress) > 0 {
result["local_address"] = localAddress
}
if reserved := parseWireGuardReserved(proxy["reserved"]); reserved != nil {
result["reserved"] = reserved
}
return result
case "vmess":
var tls any
if getBool(proxy, "tls") {
tls = map[string]any{"enabled": true, "server_name": proxy["servername"]}
}
var transport any
if getString(proxy, "network") == "ws" {
wsOpts, _ := proxy["ws-opts"].(map[string]any)
path := "/"
if wsOpts != nil {
if p, ok := wsOpts["path"].(string); ok && p != "" {
path = p
}
}
transportMap := map[string]any{
"type": "ws",
"path": path,
}
if wsOpts != nil {
if h, ok := wsOpts["headers"]; ok && h != nil {
transportMap["headers"] = h
}
}
transport = transportMap
}
return util.StripUndefined(map[string]any{
"type": "vmess",
"tag": name,
"server": server,
"server_port": port,
"uuid": proxy["uuid"],
"security": strOr(proxy, "cipher", "auto"),
"alter_id": proxy["alterId"],
"tls": tls,
"transport": transport,
})
}
return nil
}
func parseWireGuardReserved(value any) []int {
switch v := value.(type) {
case []any:
var result []int
for _, item := range v {
n := numberOrUndefined(item)
if n != 0 {
result = append(result, n)
}
}
if len(result) > 0 {
return result
}
return nil
case []int:
return v
case string:
if v == "" {
return nil
}
var result []int
for _, part := range splitComma(v) {
n := numberOrUndefined(part)
result = append(result, n)
}
return result
}
return nil
}
func splitComma(s string) []string {
var result []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == ',' {
result = append(result, s[start:i])
start = i + 1
}
}
result = append(result, s[start:])
return result
}
// Ensure fmt is used
var _ = fmt.Sprintf