Files
subconverter-go/internal/config/template_manager.go
Rogee 7fcabe0225
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
first commit
2025-09-28 10:05:07 +08:00

600 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}