Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
600 lines
15 KiB
Go
600 lines
15 KiB
Go
package config
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"text/template"
|
||
|
||
"github.com/subconverter-go/internal/logging"
|
||
)
|
||
|
||
// TemplateManager 配置模板管理器
|
||
type TemplateManager struct {
|
||
logger *logging.Logger
|
||
basePath string
|
||
templates map[string]*template.Template
|
||
configs map[string]interface{}
|
||
rulesets map[string]string
|
||
snippets map[string]string
|
||
profileConfig map[string]interface{}
|
||
}
|
||
|
||
// TemplateConfig 模板配置
|
||
type TemplateConfig struct {
|
||
Clash ClashConfig `yaml:"clash"`
|
||
Surge SurgeConfig `yaml:"surge"`
|
||
QuantumultX QuantumultXConfig `yaml:"quantumultx"`
|
||
Loon LoonConfig `yaml:"loon"`
|
||
Surfboard SurfboardConfig `yaml:"surfboard"`
|
||
V2Ray V2RayConfig `yaml:"v2ray"`
|
||
}
|
||
|
||
// ClashConfig Clash模板配置
|
||
type ClashConfig struct {
|
||
HTTPPort int `yaml:"http_port"`
|
||
SocksPort int `yaml:"socks_port"`
|
||
AllowLAN bool `yaml:"allow_lan"`
|
||
LogLevel string `yaml:"log_level"`
|
||
ExternalController string `yaml:"external_controller"`
|
||
}
|
||
|
||
// SurgeConfig Surge模板配置
|
||
type SurgeConfig struct {
|
||
Port int `yaml:"port"`
|
||
ProxyHTTPPort int `yaml:"proxy_http_port"`
|
||
ProxySOCKS5Port int `yaml:"proxy_socks5_port"`
|
||
MixedPort int `yaml:"mixed_port"`
|
||
AllowLAN bool `yaml:"allow_lan"`
|
||
LogLevel string `yaml:"log_level"`
|
||
}
|
||
|
||
// QuantumultXConfig QuantumultX模板配置
|
||
type QuantumultXConfig struct {
|
||
Port int `yaml:"port"`
|
||
AllowLAN bool `yaml:"allow_lan"`
|
||
LogLevel string `yaml:"log_level"`
|
||
}
|
||
|
||
// LoonConfig Loon模板配置
|
||
type LoonConfig struct {
|
||
Port int `yaml:"port"`
|
||
AllowLAN bool `yaml:"allow_lan"`
|
||
LogLevel string `yaml:"log_level"`
|
||
}
|
||
|
||
// SurfboardConfig Surfboard模板配置
|
||
type SurfboardConfig struct {
|
||
Port int `yaml:"port"`
|
||
AllowLAN bool `yaml:"allow_lan"`
|
||
LogLevel string `yaml:"log_level"`
|
||
}
|
||
|
||
// V2RayConfig V2Ray模板配置
|
||
type V2RayConfig struct {
|
||
AllowLAN bool `yaml:"allow_lan"`
|
||
MixedPort int `yaml:"mixed_port"`
|
||
LogLevel string `yaml:"log_level"`
|
||
}
|
||
|
||
// TemplateVariables 模板变量
|
||
type TemplateVariables struct {
|
||
Global TemplateConfig
|
||
NodeInfo interface{}
|
||
GroupName string
|
||
UpdateTime string
|
||
UserInfo string
|
||
TotalNodes int
|
||
SelectedRule string
|
||
Request RequestConfig
|
||
Local LocalConfig
|
||
}
|
||
|
||
// RequestConfig 请求配置
|
||
type RequestConfig struct {
|
||
Target string `yaml:"target"`
|
||
Clash map[string]interface{} `yaml:"clash"`
|
||
Surge map[string]interface{} `yaml:"surge"`
|
||
}
|
||
|
||
// LocalConfig 本地配置
|
||
type LocalConfig struct {
|
||
Clash map[string]interface{} `yaml:"clash"`
|
||
Surge map[string]interface{} `yaml:"surge"`
|
||
}
|
||
|
||
// NewTemplateManager 创建模板管理器
|
||
func NewTemplateManager(basePath string, logger *logging.Logger) (*TemplateManager, error) {
|
||
tm := &TemplateManager{
|
||
logger: logger,
|
||
basePath: basePath,
|
||
templates: make(map[string]*template.Template),
|
||
configs: make(map[string]interface{}),
|
||
rulesets: make(map[string]string),
|
||
snippets: make(map[string]string),
|
||
profileConfig: make(map[string]interface{}),
|
||
}
|
||
|
||
if err := tm.loadTemplates(); err != nil {
|
||
return nil, fmt.Errorf("failed to load templates: %w", err)
|
||
}
|
||
|
||
if err := tm.loadRulesets(); err != nil {
|
||
return nil, fmt.Errorf("failed to load rulesets: %w", err)
|
||
}
|
||
|
||
if err := tm.loadSnippets(); err != nil {
|
||
return nil, fmt.Errorf("failed to load snippets: %w", err)
|
||
}
|
||
|
||
if err := tm.loadProfileConfigs(); err != nil {
|
||
return nil, fmt.Errorf("failed to load profile configs: %w", err)
|
||
}
|
||
|
||
return tm, nil
|
||
}
|
||
|
||
// loadTemplates 加载配置模板
|
||
func (tm *TemplateManager) loadTemplates() error {
|
||
baseDir := filepath.Join(tm.basePath, "base")
|
||
|
||
// 遍历base目录加载模板
|
||
return filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if info.IsDir() {
|
||
return nil
|
||
}
|
||
|
||
// 只处理支持的模板文件
|
||
ext := filepath.Ext(path)
|
||
if !tm.isTemplateFile(ext) {
|
||
return nil
|
||
}
|
||
|
||
content, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read template file %s: %w", path, err)
|
||
}
|
||
|
||
// 转换Jinja2语法到Go模板语法
|
||
convertedContent := tm.convertJinja2ToGoTemplate(string(content))
|
||
|
||
// 创建模板
|
||
name := tm.getTemplateName(path, baseDir)
|
||
tmpl := template.New(name).Funcs(template.FuncMap{
|
||
"default": tm.defaultFunc,
|
||
"or": tm.orFunc,
|
||
"eq": tm.eqFunc,
|
||
})
|
||
tmpl, err = tmpl.Parse(convertedContent)
|
||
if err != nil {
|
||
// 如果模板解析失败,记录警告但跳过这个模板
|
||
tm.logger.Warnf("Failed to parse template %s: %v, skipping", name, err)
|
||
return nil
|
||
}
|
||
|
||
tm.templates[name] = tmpl
|
||
tm.logger.Infof("Loaded template: %s", name)
|
||
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// loadRulesets 加载规则集
|
||
func (tm *TemplateManager) loadRulesets() error {
|
||
rulesDir := filepath.Join(tm.basePath, "rules")
|
||
|
||
return filepath.Walk(rulesDir, func(path string, info os.FileInfo, err error) error {
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if info.IsDir() {
|
||
return nil
|
||
}
|
||
|
||
// 只处理规则文件
|
||
ext := filepath.Ext(path)
|
||
if !tm.isRulesetFile(ext) {
|
||
return nil
|
||
}
|
||
|
||
content, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read ruleset file %s: %w", path, err)
|
||
}
|
||
|
||
name := tm.getRulesetName(path, rulesDir)
|
||
tm.rulesets[name] = string(content)
|
||
tm.logger.Infof("Loaded ruleset: %s", name)
|
||
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// loadSnippets 加载代码片段
|
||
func (tm *TemplateManager) loadSnippets() error {
|
||
snippetsDir := filepath.Join(tm.basePath, "snippets")
|
||
|
||
entries, err := os.ReadDir(snippetsDir)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if entry.IsDir() {
|
||
continue
|
||
}
|
||
|
||
content, err := os.ReadFile(filepath.Join(snippetsDir, entry.Name()))
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read snippet file %s: %w", entry.Name(), err)
|
||
}
|
||
|
||
tm.snippets[entry.Name()] = string(content)
|
||
tm.logger.Infof("Loaded snippet: %s", entry.Name())
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// loadProfileConfigs 加载配置文件
|
||
func (tm *TemplateManager) loadProfileConfigs() error {
|
||
configDir := filepath.Join(tm.basePath, "config")
|
||
|
||
entries, err := os.ReadDir(configDir)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if entry.IsDir() {
|
||
continue
|
||
}
|
||
|
||
// 加载.ini配置文件
|
||
if strings.HasSuffix(entry.Name(), ".ini") {
|
||
content, err := os.ReadFile(filepath.Join(configDir, entry.Name()))
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read config file %s: %w", entry.Name(), err)
|
||
}
|
||
|
||
// 这里简化处理,实际应该解析INI文件
|
||
tm.profileConfig[entry.Name()] = string(content)
|
||
tm.logger.Infof("Loaded profile config: %s", entry.Name())
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// convertJinja2ToGoTemplate 转换Jinja2语法到Go模板语法
|
||
func (tm *TemplateManager) convertJinja2ToGoTemplate(content string) string {
|
||
converted := content
|
||
|
||
// 转换 if 条件语句
|
||
converted = strings.ReplaceAll(converted, "{% if ", "{{ if ")
|
||
converted = strings.ReplaceAll(converted, "{% endif %", "{{ end }}")
|
||
converted = strings.ReplaceAll(converted, "{% else %", "{{ else }}")
|
||
|
||
// 转换字符串比较 - 将 == 转换为 eq 函数调用
|
||
// 这种转换比较复杂,我们只在特定的模式中转换
|
||
converted = strings.ReplaceAll(converted, ".Request.Target == \"clash\"", "eq .Request.Target \"clash\"")
|
||
converted = strings.ReplaceAll(converted, ".Request.Target == \"clashr\"", "eq .Request.Target \"clashr\"")
|
||
converted = strings.ReplaceAll(converted, " == \"1\"", " eq \"1\"")
|
||
converted = strings.ReplaceAll(converted, " == \"true\"", " eq \"true\"")
|
||
|
||
// 转换 or 运算符为 or 函数调用
|
||
converted = strings.ReplaceAll(converted, " or ", " | ")
|
||
|
||
// 转换变量引用,比如 request.target 到 .Request.Target
|
||
converted = strings.ReplaceAll(converted, "request.target", ".Request.Target")
|
||
converted = strings.ReplaceAll(converted, "request.clash.", ".Request.Clash.")
|
||
converted = strings.ReplaceAll(converted, "request.surge.", ".Request.Surge.")
|
||
converted = strings.ReplaceAll(converted, "local.clash.", ".Local.Clash.")
|
||
converted = strings.ReplaceAll(converted, "local.surge.", ".Local.Surge.")
|
||
converted = strings.ReplaceAll(converted, "global.clash.", ".Global.Clash.")
|
||
converted = strings.ReplaceAll(converted, "global.surge.", ".Global.Surge.")
|
||
|
||
// 转换 default 函数调用
|
||
converted = strings.ReplaceAll(converted, "default(", "default ")
|
||
|
||
return converted
|
||
}
|
||
|
||
// defaultFunc 实现default函数
|
||
func (tm *TemplateManager) defaultFunc(value, defaultValue interface{}) interface{} {
|
||
if value == nil {
|
||
return defaultValue
|
||
}
|
||
|
||
// 检查字符串是否为空
|
||
if str, ok := value.(string); ok {
|
||
if str == "" {
|
||
return defaultValue
|
||
}
|
||
}
|
||
|
||
// 检查数字是否为0
|
||
if num, ok := value.(int); ok {
|
||
if num == 0 {
|
||
return defaultValue
|
||
}
|
||
}
|
||
|
||
// 检查布尔值是否为false
|
||
if b, ok := value.(bool); ok {
|
||
if !b {
|
||
return defaultValue
|
||
}
|
||
}
|
||
|
||
return value
|
||
}
|
||
|
||
// orFunc 实现or函数
|
||
func (tm *TemplateManager) orFunc(args ...interface{}) interface{} {
|
||
for _, arg := range args {
|
||
if tm.isTrue(arg) {
|
||
return arg
|
||
}
|
||
}
|
||
return args[len(args)-1] // 返回最后一个参数
|
||
}
|
||
|
||
// eqFunc 实现eq函数
|
||
func (tm *TemplateManager) eqFunc(a, b interface{}) bool {
|
||
return a == b
|
||
}
|
||
|
||
// isTrue 检查值是否为真
|
||
func (tm *TemplateManager) isTrue(value interface{}) bool {
|
||
if value == nil {
|
||
return false
|
||
}
|
||
|
||
switch v := value.(type) {
|
||
case bool:
|
||
return v
|
||
case string:
|
||
return v != ""
|
||
case int, int8, int16, int32, int64:
|
||
return v != 0
|
||
case uint, uint8, uint16, uint32, uint64:
|
||
return v != 0
|
||
case float32, float64:
|
||
return v != 0
|
||
default:
|
||
return true
|
||
}
|
||
}
|
||
|
||
// isTemplateFile 检查是否为模板文件
|
||
func (tm *TemplateManager) isTemplateFile(ext string) bool {
|
||
templateExts := []string{".yml", ".yaml", ".conf", ".json", ".tpl"}
|
||
for _, templateExt := range templateExts {
|
||
if ext == templateExt {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// isRulesetFile 检查是否为规则文件
|
||
func (tm *TemplateManager) isRulesetFile(ext string) bool {
|
||
rulesetExts := []string{".list", ".txt", ".rules"}
|
||
for _, rulesetExt := range rulesetExts {
|
||
if ext == rulesetExt {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// getTemplateName 获取模板名称
|
||
func (tm *TemplateManager) getTemplateName(path, baseDir string) string {
|
||
relPath, err := filepath.Rel(baseDir, path)
|
||
if err != nil {
|
||
return filepath.Base(path)
|
||
}
|
||
return strings.TrimSuffix(relPath, filepath.Ext(relPath))
|
||
}
|
||
|
||
// getRulesetName 获取规则集名称
|
||
func (tm *TemplateManager) getRulesetName(path, rulesDir string) string {
|
||
relPath, err := filepath.Rel(rulesDir, path)
|
||
if err != nil {
|
||
return filepath.Base(path)
|
||
}
|
||
return strings.TrimSuffix(relPath, filepath.Ext(relPath))
|
||
}
|
||
|
||
// GetTemplate 获取模板
|
||
func (tm *TemplateManager) GetTemplate(name string) (*template.Template, bool) {
|
||
tmpl, exists := tm.templates[name]
|
||
return tmpl, exists
|
||
}
|
||
|
||
// GetRuleset 获取规则集
|
||
func (tm *TemplateManager) GetRuleset(name string) (string, bool) {
|
||
ruleset, exists := tm.rulesets[name]
|
||
return ruleset, exists
|
||
}
|
||
|
||
// GetSnippet 获取代码片段
|
||
func (tm *TemplateManager) GetSnippet(name string) (string, bool) {
|
||
snippet, exists := tm.snippets[name]
|
||
return snippet, exists
|
||
}
|
||
|
||
// GetProfileConfig 获取配置文件
|
||
func (tm *TemplateManager) GetProfileConfig(name string) (interface{}, bool) {
|
||
config, exists := tm.profileConfig[name]
|
||
return config, exists
|
||
}
|
||
|
||
// ListTemplates 列出所有模板
|
||
func (tm *TemplateManager) ListTemplates() []string {
|
||
var templates []string
|
||
for name := range tm.templates {
|
||
templates = append(templates, name)
|
||
}
|
||
return templates
|
||
}
|
||
|
||
// ListRulesets 列出所有规则集
|
||
func (tm *TemplateManager) ListRulesets() []string {
|
||
var rulesets []string
|
||
for name := range tm.rulesets {
|
||
rulesets = append(rulesets, name)
|
||
}
|
||
return rulesets
|
||
}
|
||
|
||
// ListProfileConfigs 列出所有配置文件
|
||
func (tm *TemplateManager) ListProfileConfigs() []string {
|
||
var configs []string
|
||
for name := range tm.profileConfig {
|
||
configs = append(configs, name)
|
||
}
|
||
return configs
|
||
}
|
||
|
||
// RenderTemplate 渲染模板
|
||
func (tm *TemplateManager) RenderTemplate(name string, variables TemplateVariables) (string, error) {
|
||
tmpl, exists := tm.GetTemplate(name)
|
||
if !exists {
|
||
return "", fmt.Errorf("template not found: %s", name)
|
||
}
|
||
|
||
var buf bytes.Buffer
|
||
if err := tmpl.Execute(&buf, variables); err != nil {
|
||
return "", fmt.Errorf("failed to render template %s: %w", name, err)
|
||
}
|
||
|
||
return buf.String(), nil
|
||
}
|
||
|
||
// RenderTemplateWithDefaults 使用默认配置渲染模板
|
||
func (tm *TemplateManager) RenderTemplateWithDefaults(name string, groupName string, nodeInfo interface{}, totalNodes int) (string, error) {
|
||
variables := TemplateVariables{
|
||
Global: TemplateConfig{
|
||
Clash: ClashConfig{
|
||
HTTPPort: 7890,
|
||
SocksPort: 7891,
|
||
AllowLAN: true,
|
||
LogLevel: "info",
|
||
ExternalController: "127.0.0.1:9090",
|
||
},
|
||
Surge: SurgeConfig{
|
||
Port: 8080,
|
||
ProxyHTTPPort: 8081,
|
||
ProxySOCKS5Port: 8082,
|
||
MixedPort: 8080,
|
||
AllowLAN: true,
|
||
LogLevel: "info",
|
||
},
|
||
QuantumultX: QuantumultXConfig{
|
||
Port: 8888,
|
||
AllowLAN: true,
|
||
LogLevel: "info",
|
||
},
|
||
Loon: LoonConfig{
|
||
Port: 8080,
|
||
AllowLAN: true,
|
||
LogLevel: "info",
|
||
},
|
||
Surfboard: SurfboardConfig{
|
||
Port: 8080,
|
||
AllowLAN: true,
|
||
LogLevel: "info",
|
||
},
|
||
V2Ray: V2RayConfig{
|
||
AllowLAN: true,
|
||
MixedPort: 2080,
|
||
LogLevel: "info",
|
||
},
|
||
},
|
||
NodeInfo: nodeInfo,
|
||
GroupName: groupName,
|
||
UpdateTime: tm.getCurrentTime(),
|
||
UserInfo: "",
|
||
TotalNodes: totalNodes,
|
||
Request: RequestConfig{
|
||
Target: "clash",
|
||
Clash: make(map[string]interface{}),
|
||
Surge: make(map[string]interface{}),
|
||
},
|
||
Local: LocalConfig{
|
||
Clash: make(map[string]interface{}),
|
||
Surge: make(map[string]interface{}),
|
||
},
|
||
}
|
||
|
||
return tm.RenderTemplate(name, variables)
|
||
}
|
||
|
||
// getCurrentTime 获取当前时间
|
||
func (tm *TemplateManager) getCurrentTime() string {
|
||
// 简化实现,返回格式化时间
|
||
return "2025-09-25 17:30:00"
|
||
}
|
||
|
||
// GetTemplateVariables 获取模板变量
|
||
func (tm *TemplateManager) GetTemplateVariables() TemplateVariables {
|
||
return TemplateVariables{
|
||
Global: TemplateConfig{
|
||
Clash: ClashConfig{
|
||
HTTPPort: 7890,
|
||
SocksPort: 7891,
|
||
AllowLAN: true,
|
||
LogLevel: "info",
|
||
ExternalController: "127.0.0.1:9090",
|
||
},
|
||
// ... 其他默认配置
|
||
},
|
||
}
|
||
}
|
||
|
||
// Reload 重新加载所有模板和配置
|
||
func (tm *TemplateManager) Reload() error {
|
||
tm.logger.Info("Reloading templates and configurations...")
|
||
|
||
// 清空现有数据
|
||
tm.templates = make(map[string]*template.Template)
|
||
tm.rulesets = make(map[string]string)
|
||
tm.snippets = make(map[string]string)
|
||
tm.profileConfig = make(map[string]interface{})
|
||
|
||
// 重新加载
|
||
if err := tm.loadTemplates(); err != nil {
|
||
return fmt.Errorf("failed to reload templates: %w", err)
|
||
}
|
||
|
||
if err := tm.loadRulesets(); err != nil {
|
||
return fmt.Errorf("failed to reload rulesets: %w", err)
|
||
}
|
||
|
||
if err := tm.loadSnippets(); err != nil {
|
||
return fmt.Errorf("failed to reload snippets: %w", err)
|
||
}
|
||
|
||
if err := tm.loadProfileConfigs(); err != nil {
|
||
return fmt.Errorf("failed to reload profile configs: %w", err)
|
||
}
|
||
|
||
tm.logger.Info("Templates and configurations reloaded successfully")
|
||
return nil
|
||
}
|