Files
subconverter-go/internal/handler/template_handler.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

801 lines
21 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 handler
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/subconverter-go/internal/config"
"github.com/subconverter-go/internal/logging"
"github.com/gofiber/fiber/v2"
)
// TemplateHandler 模板处理器
type TemplateHandler struct {
templateManager *config.TemplateManager
logger *logging.Logger
configManager *config.ConfigManager
}
// TemplateInfo 模板信息
type TemplateInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Size int64 `json:"size"`
}
// RulesetInfo 规则集信息
type RulesetInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
RuleCount int `json:"rule_count"`
}
// TemplateListRequest 模板列表请求
type TemplateListRequest struct {
Type string `query:"type"` // 模板类型: clash, surge, quanx, loon, surfboard, v2ray
Format string `query:"format"` // 格式: json, yaml
Profile string `query:"profile"` // 配置文件名
}
// TemplateRenderRequest 模板渲染请求
type TemplateRenderRequest struct {
Template string `json:"template"` // 模板名称
Profile string `json:"profile"` // 配置文件名
Variables map[string]interface{} `json:"variables"` // 自定义变量
Target string `json:"target"` // 目标格式
NodeData interface{} `json:"node_data"` // 节点数据
GroupName string `json:"group_name"` // 组名
}
// TemplateRenderResponse 模板渲染响应
type TemplateRenderResponse struct {
Success bool `json:"success"`
Content string `json:"content"`
Variables map[string]interface{} `json:"variables"`
Metadata map[string]interface{} `json:"metadata"`
Error string `json:"error,omitempty"`
}
// NewTemplateHandler 创建模板处理器
func NewTemplateHandler(templateManager *config.TemplateManager, logger *logging.Logger, configManager *config.ConfigManager) *TemplateHandler {
return &TemplateHandler{
templateManager: templateManager,
logger: logger,
configManager: configManager,
}
}
// RegisterRoutes 注册路由
func (h *TemplateHandler) RegisterRoutes(app *fiber.App) {
// 原项目兼容性路由 - /render 端点
app.Get("/render", h.RenderDirect)
// 模板相关路由
app.Get("/api/templates", h.ListTemplates)
app.Get("/api/templates/:name", h.GetTemplate)
app.Post("/api/templates/render", h.RenderTemplate)
app.Get("/api/templates/:name/preview", h.PreviewTemplate)
// 规则集相关路由
app.Get("/api/rulesets", h.ListRulesets)
app.Get("/api/rulesets/:name", h.GetRuleset)
app.Get("/api/rulesets/:name/content", h.GetRulesetContent)
// 配置文件相关路由
app.Get("/api/profiles", h.ListProfiles)
app.Get("/api/profiles/:name", h.GetProfile)
app.Post("/api/profiles/:name/apply", h.ApplyProfile)
// 代码片段相关路由
app.Get("/api/snippets", h.ListSnippets)
app.Get("/api/snippets/:name", h.GetSnippet)
// 模板管理路由
app.Post("/api/templates/reload", h.ReloadTemplates)
app.Get("/api/templates/info", h.GetTemplatesInfo)
}
// RenderDirect 直接渲染模板(原项目兼容性端点)
func (h *TemplateHandler) RenderDirect(c *fiber.Ctx) error {
path := c.Query("path")
if path == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "path parameter is required",
})
}
// 验证路径安全性,防止目录遍历攻击
if strings.Contains(path, "..") || strings.HasPrefix(path, "/") {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Invalid path",
})
}
// 从模板路径中提取模板名称
templateName := strings.TrimSuffix(path, filepath.Ext(path))
// 检查模板是否存在
if _, exists := h.templateManager.GetTemplate(templateName); !exists {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Template '%s' not found", templateName),
})
}
// 准备模板变量从URL参数中获取
variables := h.prepareTemplateVariables(c)
// 渲染模板
content, err := h.templateManager.RenderTemplate(templateName, variables)
if err != nil {
h.logger.Errorf("Failed to render template %s: %v", templateName, err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to render template: %v", err),
})
}
// 根据模板类型设置Content-Type
contentType := h.getContentType(templateName)
c.Set("Content-Type", contentType)
return c.SendString(content)
}
// prepareTemplateVariables 准备模板变量
func (h *TemplateHandler) prepareTemplateVariables(c *fiber.Ctx) config.TemplateVariables {
// 从查询参数中提取模板变量
requestConfig := config.RequestConfig{
Target: c.Query("target", "clash"),
Clash: make(map[string]interface{}),
Surge: make(map[string]interface{}),
}
// 处理额外的查询参数
for key, value := range c.Queries() {
switch {
case strings.HasPrefix(key, "clash."):
paramName := strings.TrimPrefix(key, "clash.")
requestConfig.Clash[paramName] = value
case strings.HasPrefix(key, "surge."):
paramName := strings.TrimPrefix(key, "surge.")
requestConfig.Surge[paramName] = value
}
}
localConfig := config.LocalConfig{
Clash: make(map[string]interface{}),
Surge: make(map[string]interface{}),
}
// 获取全局配置
globalConfig := config.TemplateConfig{
Clash: config.ClashConfig{
HTTPPort: 7890,
SocksPort: 7891,
AllowLAN: true,
LogLevel: "info",
ExternalController: "127.0.0.1:9090",
},
Surge: config.SurgeConfig{
Port: 8080,
ProxyHTTPPort: 8081,
ProxySOCKS5Port: 8082,
MixedPort: 8080,
AllowLAN: true,
LogLevel: "info",
},
// 可以添加其他客户端的默认配置
}
return config.TemplateVariables{
Global: globalConfig,
NodeInfo: h.getSampleNodeData(),
GroupName: c.Query("group", "Proxy"),
UpdateTime: time.Now().Format("2006-01-02 15:04:05"),
UserInfo: "",
TotalNodes: 1,
Request: requestConfig,
Local: localConfig,
}
}
// ListTemplates 列出所有模板
func (h *TemplateHandler) ListTemplates(c *fiber.Ctx) error {
var req TemplateListRequest
if err := c.QueryParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Invalid request parameters",
})
}
templates := h.templateManager.ListTemplates()
var filteredTemplates []TemplateInfo
for _, name := range templates {
// 根据类型过滤
if req.Type != "" {
if !h.isTemplateOfType(name, req.Type) {
continue
}
}
templateInfo := TemplateInfo{
Name: name,
Type: h.getTemplateType(name),
Description: h.getTemplateDescription(name),
}
// 获取模板文件大小
if content, err := h.templateManager.RenderTemplate(name, config.TemplateVariables{}); err == nil {
templateInfo.Size = int64(len(content))
}
filteredTemplates = append(filteredTemplates, templateInfo)
}
// 根据格式返回不同响应
if req.Format == "yaml" {
return c.JSON(filteredTemplates)
}
return c.JSON(fiber.Map{
"success": true,
"templates": filteredTemplates,
"total": len(filteredTemplates),
})
}
// GetTemplate 获取特定模板
func (h *TemplateHandler) GetTemplate(c *fiber.Ctx) error {
name := c.Params("name")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Template name is required",
})
}
_, exists := h.templateManager.GetTemplate(name)
if !exists {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": "Template not found",
})
}
// 返回模板信息
templateInfo := fiber.Map{
"name": name,
"type": h.getTemplateType(name),
"description": h.getTemplateDescription(name),
"exists": true,
}
return c.JSON(fiber.Map{
"success": true,
"template": templateInfo,
})
}
// RenderTemplate 渲染模板
func (h *TemplateHandler) RenderTemplate(c *fiber.Ctx) error {
var req TemplateRenderRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Invalid request body",
})
}
// 验证必需参数
if req.Template == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Template name is required",
})
}
// 准备模板变量
variables := config.TemplateVariables{
GroupName: req.GroupName,
NodeInfo: req.NodeData,
TotalNodes: h.getTotalNodes(req.NodeData),
}
// 应用自定义变量
if req.Variables != nil {
if global, ok := req.Variables["global"]; ok {
if globalMap, ok := global.(map[string]interface{}); ok {
variables.Global = h.parseGlobalConfig(globalMap)
}
}
}
// 渲染模板
content, err := h.templateManager.RenderTemplateWithDefaults(
req.Template,
variables.GroupName,
variables.NodeInfo,
variables.TotalNodes,
)
if err != nil {
h.logger.Errorf("Failed to render template %s: %v", req.Template, err)
return c.Status(fiber.StatusInternalServerError).JSON(TemplateRenderResponse{
Success: false,
Error: fmt.Sprintf("Failed to render template: %v", err),
})
}
// 构建响应
response := TemplateRenderResponse{
Success: true,
Content: content,
Variables: map[string]interface{}{
"group_name": variables.GroupName,
"total_nodes": variables.TotalNodes,
"template": req.Template,
},
Metadata: map[string]interface{}{
"template_name": req.Template,
"target": req.Target,
"group_name": req.GroupName,
"node_count": variables.TotalNodes,
},
}
return c.JSON(response)
}
// PreviewTemplate 预览模板
func (h *TemplateHandler) PreviewTemplate(c *fiber.Ctx) error {
name := c.Params("name")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Template name is required",
})
}
// 使用示例数据预览
sampleData := h.getSampleNodeData()
content, err := h.templateManager.RenderTemplateWithDefaults(
name,
"SampleGroup",
sampleData,
1,
)
if err != nil {
h.logger.Errorf("Failed to preview template %s: %v", name, err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to preview template: %v", err),
})
}
// 根据模板类型设置Content-Type
contentType := h.getContentType(name)
c.Set("Content-Type", contentType)
return c.SendString(content)
}
// ListRulesets 列出所有规则集
func (h *TemplateHandler) ListRulesets(c *fiber.Ctx) error {
rulesets := h.templateManager.ListRulesets()
var rulesetInfos []RulesetInfo
for _, name := range rulesets {
if content, exists := h.templateManager.GetRuleset(name); exists {
rulesetInfos = append(rulesetInfos, RulesetInfo{
Name: name,
Type: h.getRulesetType(name),
Description: h.getRulesetDescription(name),
RuleCount: h.countRules(content),
})
}
}
return c.JSON(fiber.Map{
"success": true,
"rulesets": rulesetInfos,
"total": len(rulesetInfos),
})
}
// GetRuleset 获取规则集信息
func (h *TemplateHandler) GetRuleset(c *fiber.Ctx) error {
name := c.Params("name")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Ruleset name is required",
})
}
content, exists := h.templateManager.GetRuleset(name)
if !exists {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": "Ruleset not found",
})
}
rulesetInfo := RulesetInfo{
Name: name,
Type: h.getRulesetType(name),
Description: h.getRulesetDescription(name),
RuleCount: h.countRules(content),
}
return c.JSON(fiber.Map{
"success": true,
"ruleset": rulesetInfo,
})
}
// GetRulesetContent 获取规则集内容
func (h *TemplateHandler) GetRulesetContent(c *fiber.Ctx) error {
name := c.Params("name")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Ruleset name is required",
})
}
content, exists := h.templateManager.GetRuleset(name)
if !exists {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": "Ruleset not found",
})
}
c.Set("Content-Type", "text/plain")
return c.SendString(content)
}
// ListProfiles 列出所有配置文件
func (h *TemplateHandler) ListProfiles(c *fiber.Ctx) error {
profiles := h.templateManager.ListProfileConfigs()
return c.JSON(fiber.Map{
"success": true,
"profiles": profiles,
"total": len(profiles),
})
}
// GetProfile 获取配置文件
func (h *TemplateHandler) GetProfile(c *fiber.Ctx) error {
name := c.Params("name")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Profile name is required",
})
}
profile, exists := h.templateManager.GetProfileConfig(name)
if !exists {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": "Profile not found",
})
}
return c.JSON(fiber.Map{
"success": true,
"profile": profile,
})
}
// ApplyProfile 应用配置文件
func (h *TemplateHandler) ApplyProfile(c *fiber.Ctx) error {
name := c.Params("name")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Profile name is required",
})
}
// 这里简化处理,实际应该解析并应用配置文件
_, exists := h.templateManager.GetProfileConfig(name)
if !exists {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": "Profile not found",
})
}
h.logger.Infof("Applied profile: %s", name)
return c.JSON(fiber.Map{
"success": true,
"message": fmt.Sprintf("Profile '%s' applied successfully", name),
})
}
// ListSnippets 列出所有代码片段
func (h *TemplateHandler) ListSnippets(c *fiber.Ctx) error {
snippets := make(map[string]string)
// 这里简化处理实际应该从templateManager获取
snippets["groups.txt"] = "Proxy group configuration snippets"
snippets["rename_node.txt"] = "Node renaming snippets"
return c.JSON(fiber.Map{
"success": true,
"snippets": snippets,
"total": len(snippets),
})
}
// GetSnippet 获取代码片段
func (h *TemplateHandler) GetSnippet(c *fiber.Ctx) error {
name := c.Params("name")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Snippet name is required",
})
}
snippet, exists := h.templateManager.GetSnippet(name)
if !exists {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": "Snippet not found",
})
}
c.Set("Content-Type", "text/plain")
return c.SendString(snippet)
}
// ReloadTemplates 重新加载模板
func (h *TemplateHandler) ReloadTemplates(c *fiber.Ctx) error {
if err := h.templateManager.Reload(); err != nil {
h.logger.Errorf("Failed to reload templates: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to reload templates: %v", err),
})
}
h.logger.Info("Templates reloaded successfully")
return c.JSON(fiber.Map{
"success": true,
"message": "Templates reloaded successfully",
})
}
// GetTemplatesInfo 获取模板系统信息
func (h *TemplateHandler) GetTemplatesInfo(c *fiber.Ctx) error {
templates := h.templateManager.ListTemplates()
rulesets := h.templateManager.ListRulesets()
profiles := h.templateManager.ListProfileConfigs()
info := fiber.Map{
"template_count": len(templates),
"ruleset_count": len(rulesets),
"profile_count": len(profiles),
"base_path": h.configManager.GetBasePath(),
"supported_types": []string{"clash", "surge", "quanx", "loon", "surfboard", "v2ray"},
}
return c.JSON(fiber.Map{
"success": true,
"info": info,
})
}
// 辅助方法
// isTemplateOfType 检查模板是否为指定类型
func (h *TemplateHandler) isTemplateOfType(name, templateType string) bool {
return strings.Contains(strings.ToLower(name), strings.ToLower(templateType))
}
// getTemplateType 获取模板类型
func (h *TemplateHandler) getTemplateType(name string) string {
name = strings.ToLower(name)
switch {
case strings.Contains(name, "clash"):
return "clash"
case strings.Contains(name, "surge"):
return "surge"
case strings.Contains(name, "quanx"):
return "quanx"
case strings.Contains(name, "loon"):
return "loon"
case strings.Contains(name, "surfboard"):
return "surfboard"
case strings.Contains(name, "v2ray"):
return "v2ray"
default:
return "unknown"
}
}
// getTemplateDescription 获取模板描述
func (h *TemplateHandler) getTemplateDescription(name string) string {
descriptions := map[string]string{
"GeneralClashConfig": "General Clash configuration template",
"forcerule": "Force rule configuration template",
"surge.conf": "Surge configuration template",
"quan.conf": "Quantumult configuration template",
"loon.conf": "Loon configuration template",
"surfboard.conf": "Surfboard configuration template",
}
if desc, exists := descriptions[name]; exists {
return desc
}
return fmt.Sprintf("Template: %s", name)
}
// getRulesetType 获取规则集类型
func (h *TemplateHandler) getRulesetType(name string) string {
ext := filepath.Ext(name)
switch ext {
case ".list":
return "domain_list"
case ".txt":
return "text_rules"
case ".rules":
return "rules"
default:
return "unknown"
}
}
// getRulesetDescription 获取规则集描述
func (h *TemplateHandler) getRulesetDescription(name string) string {
// 这里可以根据规则集名称返回描述
return fmt.Sprintf("Ruleset: %s", name)
}
// countRules 计算规则数量
func (h *TemplateHandler) countRules(content string) int {
lines := strings.Split(content, "\n")
count := 0
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
count++
}
}
return count
}
// parseGlobalConfig 解析全局配置
func (h *TemplateHandler) parseGlobalConfig(cfg map[string]interface{}) config.TemplateConfig {
var globalConfig config.TemplateConfig
// 解析Clash配置
if clash, ok := cfg["clash"].(map[string]interface{}); ok {
globalConfig.Clash.HTTPPort = h.getInt(clash, "http_port", 7890)
globalConfig.Clash.SocksPort = h.getInt(clash, "socks_port", 7891)
globalConfig.Clash.AllowLAN = h.getBool(clash, "allow_lan", true)
globalConfig.Clash.LogLevel = h.getString(clash, "log_level", "info")
globalConfig.Clash.ExternalController = h.getString(clash, "external_controller", "127.0.0.1:9090")
}
// 解析Surge配置
if surge, ok := cfg["surge"].(map[string]interface{}); ok {
globalConfig.Surge.Port = h.getInt(surge, "port", 8080)
globalConfig.Surge.ProxyHTTPPort = h.getInt(surge, "proxy_http_port", 8081)
globalConfig.Surge.ProxySOCKS5Port = h.getInt(surge, "proxy_socks5_port", 8082)
globalConfig.Surge.MixedPort = h.getInt(surge, "mixed_port", 8080)
globalConfig.Surge.AllowLAN = h.getBool(surge, "allow_lan", true)
globalConfig.Surge.LogLevel = h.getString(surge, "log_level", "info")
}
return globalConfig
}
// getInt 获取整数值
func (h *TemplateHandler) getInt(m map[string]interface{}, key string, defaultValue int) int {
if val, ok := m[key]; ok {
switch v := val.(type) {
case float64:
return int(v)
case int:
return v
case string:
if i, err := strconv.Atoi(v); err == nil {
return i
}
}
}
return defaultValue
}
// getBool 获取布尔值
func (h *TemplateHandler) getBool(m map[string]interface{}, key string, defaultValue bool) bool {
if val, ok := m[key]; ok {
switch v := val.(type) {
case bool:
return v
case string:
if b, err := strconv.ParseBool(v); err == nil {
return b
}
}
}
return defaultValue
}
// getString 获取字符串值
func (h *TemplateHandler) getString(m map[string]interface{}, key string, defaultValue string) string {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
return defaultValue
}
// getTotalNodes 获取节点总数
func (h *TemplateHandler) getTotalNodes(nodeData interface{}) int {
if nodeData == nil {
return 0
}
// 根据不同的数据结构计算节点数量
switch data := nodeData.(type) {
case []interface{}:
return len(data)
case map[string]interface{}:
if proxies, ok := data["proxies"].([]interface{}); ok {
return len(proxies)
}
}
return 1 // 默认返回1
}
// getSampleNodeData 获取示例节点数据
func (h *TemplateHandler) getSampleNodeData() interface{} {
return map[string]interface{}{
"remark": "Sample Server",
"hostname": "sample.example.com",
"port": 443,
"type": "shadowsocks",
"settings": map[string]interface{}{
"method": "aes-256-gcm",
"password": "sample-password",
},
}
}
// getContentType 获取内容类型
func (h *TemplateHandler) getContentType(name string) string {
ext := filepath.Ext(name)
switch ext {
case ".yml", ".yaml":
return "application/x-yaml"
case ".json":
return "application/json"
case ".conf":
return "text/plain"
default:
return "text/plain"
}
}