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,801 @@
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"
}
}