feat: migrate to golang

This commit is contained in:
2026-07-19 20:01:38 +08:00
parent 302d4e6bb5
commit a2630df9e0
69 changed files with 4750 additions and 3369 deletions

324
internal/corecfg/manager.go Normal file
View File

@ -0,0 +1,324 @@
// Package corecfg manages the user's core (mihomo/clash) configuration:
// import, export, edit, reset, show, and applying a subscription to produce
// the final generated config file.
package corecfg
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
"gitea.epss.net.cn/klesh/ss/internal/editor"
"gitea.epss.net.cn/klesh/ss/internal/model"
"gitea.epss.net.cn/klesh/ss/internal/storage"
"gitea.epss.net.cn/klesh/ss/internal/subscription"
"gitea.epss.net.cn/klesh/ss/internal/yamlutil"
"gitea.epss.net.cn/klesh/ss/templates"
)
// Manager manages user configuration with import, export, and edit operations.
type Manager struct {
SubscriptionManager *subscription.Manager
Storage *storage.Manager
ConfigFile string
GeneratedPath string
SubscriptionName string
}
// New creates a CoreConfigManager. configFile/outputFile/subscriptionName may
// be empty to use the defaults (mirrors CoreConfigManager.__init__).
func New(subMgr *subscription.Manager, configFile, outputFile, subscriptionName string) (*Manager, error) {
m := &Manager{
SubscriptionManager: subMgr,
Storage: subMgr.Storage,
SubscriptionName: subscriptionName,
}
if configFile != "" {
abs, err := resolvePath(configFile)
if err != nil {
return nil, err
}
m.ConfigFile = abs
} else {
m.ConfigFile = filepath.Join(subMgr.Storage.ConfigDir, "core-config.yaml")
}
if outputFile != "" {
abs, err := resolvePath(outputFile)
if err != nil {
return nil, err
}
m.GeneratedPath = abs
} else {
m.GeneratedPath = filepath.Join(subMgr.Storage.ConfigDir, "generated_config.yaml")
}
return m, nil
}
func resolvePath(p string) (string, error) {
if strings.HasPrefix(p, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
p = filepath.Join(home, strings.TrimPrefix(p, "~"))
}
return filepath.Abs(p)
}
func (m *Manager) ensureConfigExists() bool {
if _, err := os.Stat(m.ConfigFile); err == nil {
return true
}
if err := os.MkdirAll(m.Storage.ConfigDir, 0o755); err != nil {
fmt.Printf("❌ Failed to create config directory: %v\n", err)
return false
}
if err := os.WriteFile(m.ConfigFile, templates.DefaultCoreConfig, 0o644); err != nil {
fmt.Printf("❌ Failed to create default config: %v\n", err)
return false
}
fmt.Printf("✅ Created default config at: %s\n", m.ConfigFile)
return true
}
// LoadConfig loads configuration from the YAML file as a raw map.
func (m *Manager) LoadConfig() map[string]interface{} {
raw, err := os.ReadFile(m.ConfigFile)
if err != nil {
return map[string]interface{}{}
}
var data map[string]interface{}
if err := yamlutil.Unmarshal(raw, &data); err != nil {
fmt.Printf("Warning: Failed to load config: %v\n", err)
return map[string]interface{}{}
}
if data == nil {
return map[string]interface{}{}
}
return data
}
// SaveConfig writes configuration to the YAML file.
func (m *Manager) SaveConfig(config map[string]interface{}) bool {
out, err := yaml.Marshal(config)
if err != nil {
fmt.Printf("Error: Failed to save config: %v\n", err)
return false
}
if err := os.WriteFile(m.ConfigFile, out, 0o644); err != nil {
fmt.Printf("Error: Failed to save config: %v\n", err)
return false
}
return true
}
// ImportConfig imports configuration from a YAML file, validating it before
// persisting.
func (m *Manager) ImportConfig(sourcePath string) bool {
raw, err := os.ReadFile(sourcePath)
if err != nil {
fmt.Printf("❌ Source file not found: %s\n", sourcePath)
return false
}
var data map[string]interface{}
if err := yamlutil.Unmarshal(raw, &data); err != nil {
fmt.Printf("❌ Invalid YAML: %v\n", err)
return false
}
if data == nil {
fmt.Println("❌ Invalid YAML format")
return false
}
if err := validateConfigMap(data); err != nil {
fmt.Printf("❌ Failed to import: %v\n", err)
return false
}
m.SaveConfig(data)
fmt.Printf("✅ Imported configuration from: %s\n", sourcePath)
return true
}
// ExportConfig exports the current configuration to a YAML file.
func (m *Manager) ExportConfig(destinationPath string) bool {
config := m.LoadConfig()
if err := os.MkdirAll(filepath.Dir(destinationPath), 0o755); err != nil {
fmt.Printf("❌ Failed to export: %v\n", err)
return false
}
out, err := yaml.Marshal(config)
if err != nil {
fmt.Printf("❌ Failed to export: %v\n", err)
return false
}
if err := os.WriteFile(destinationPath, out, 0o644); err != nil {
fmt.Printf("❌ Failed to export: %v\n", err)
return false
}
fmt.Printf("✅ Exported configuration to: %s\n", destinationPath)
return true
}
// EditConfig opens the configuration in the system default editor, backing
// it up first and restoring on failure to open the editor.
func (m *Manager) EditConfig() bool {
if !m.ensureConfigExists() {
return false
}
backupPath := strings.TrimSuffix(m.ConfigFile, filepath.Ext(m.ConfigFile)) + ".yaml.backup"
if _, err := os.Stat(m.ConfigFile); err == nil {
if data, err := os.ReadFile(m.ConfigFile); err == nil {
os.WriteFile(backupPath, data, 0o644)
}
}
if !openInEditor(m.ConfigFile) {
return false
}
m.LoadConfig()
fmt.Println("✅ Configuration edited successfully")
return true
}
// ResetConfig resets configuration to default values.
func (m *Manager) ResetConfig() bool {
if err := os.WriteFile(m.ConfigFile, templates.DefaultCoreConfig, 0o644); err != nil {
fmt.Printf("❌ Failed to reset configuration: %v\n", err)
return false
}
fmt.Println("✅ Configuration reset to default values")
return true
}
// ShowConfig prints the current configuration.
func (m *Manager) ShowConfig() {
config := m.LoadConfig()
out, _ := yaml.Marshal(config)
fmt.Println(string(out))
}
var essentialDefaults = map[string]interface{}{
"port": 7890,
"socks-port": 7891,
"mixed-port": 7890,
"allow-lan": false,
"mode": "rule",
"log-level": "info",
"external-controller": "127.0.0.1:9090",
"ipv6": true,
}
func defaultDNSConfig() map[string]interface{} {
return map[string]interface{}{
"enable": true,
"listen": "0.0.0.0:53",
"enhanced-mode": "fake-ip",
"fake-ip-range": "198.18.0.1/16",
"nameserver": []interface{}{
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query",
},
"fallback": []interface{}{
"https://1.1.1.1/dns-query",
"https://8.8.8.8/dns-query",
},
}
}
// Apply applies the active (or explicitly selected) subscription to generate
// the final config file, merging it with user configuration and filling in
// essential defaults, then executes any registered hooks.
func (m *Manager) Apply() bool {
config := m.LoadConfig()
var sub = m.resolveSubscription()
if sub == nil {
return false
}
filePath := sub.GetFilePath(m.Storage.ConfigDir)
if _, err := os.Stat(filePath); err != nil {
fmt.Printf("❌ Subscription file not found for '%s'. Please refresh the subscription first.\n", sub.Name)
return false
}
subscriptionContent, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("❌ Failed to apply configuration: %v\n", err)
return false
}
var subscriptionData map[string]interface{}
if err := yamlutil.Unmarshal(subscriptionContent, &subscriptionData); err != nil {
fmt.Printf("❌ Invalid YAML in subscription: %v\n", err)
return false
}
if subscriptionData == nil {
subscriptionData = map[string]interface{}{}
}
finalConfig := deepMerge(subscriptionData, config)
for key, defaultValue := range essentialDefaults {
if _, ok := finalConfig[key]; !ok {
finalConfig[key] = defaultValue
}
}
if _, ok := finalConfig["dns"]; !ok {
finalConfig["dns"] = defaultDNSConfig()
}
out, err := yaml.Marshal(finalConfig)
if err != nil {
fmt.Printf("❌ Failed to apply configuration: %v\n", err)
return false
}
if err := os.WriteFile(m.GeneratedPath, out, 0o644); err != nil {
fmt.Printf("❌ Failed to apply configuration: %v\n", err)
return false
}
fmt.Printf("✅ Generated final configuration: %s\n", m.GeneratedPath)
fmt.Printf(" Active subscription: %s\n", sub.Name)
m.executeHooks(m.GeneratedPath)
return true
}
func (m *Manager) resolveSubscription() *model.Subscription {
if m.SubscriptionName != "" {
sub, ok := m.SubscriptionManager.Data.Subscriptions[m.SubscriptionName]
if !ok {
fmt.Printf("❌ Subscription '%s' not found\n", m.SubscriptionName)
return nil
}
return sub
}
sub := m.SubscriptionManager.Data.GetActiveSubscription()
if sub == nil {
fmt.Println("❌ No active subscription found")
return nil
}
return sub
}
func openInEditor(path string) bool {
return editor.OpenFileInEditor(path)
}