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
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:
599
internal/config/template_manager.go
Normal file
599
internal/config/template_manager.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user