first commit
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

This commit is contained in:
Rogee
2025-09-28 10:05:07 +08:00
commit 7fcabe0225
481 changed files with 125127 additions and 0 deletions

View File

@@ -0,0 +1,599 @@
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
}