package router import ( "fmt" "net/http" "os" "path/filepath" "strings" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type agentStaticPaths struct { binDir string deployDir string } var agentBinaryFiles = map[string]string{ "linux-amd64": "aiops-agent-linux-amd64", "linux-arm64": "aiops-agent-linux-arm64", "windows-amd64": "aiops-agent-windows-amd64.exe", } // registerAgentDownloads 注册 Agent 安装脚本和二进制下载路由,避免维护 backend/agent 重复目录。 // Logan.Li func registerAgentDownloads(r *gin.Engine, log *zap.Logger) { paths := resolveAgentStaticPaths(log) r.GET("/agent/arch", func(c *gin.Context) { base := requestBaseURL(c) if base == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "request host is required"}) return } downloads := availableAgentDownloads(paths.binDir) c.JSON(http.StatusOK, gin.H{ "version": "1.0.0", "downloads": downloads, "install": fmt.Sprintf("curl -fsSL %s/agent/install.sh | sudo bash -s -- --server %s --token ", base, base), }) }) r.GET("/agent/install.sh", func(c *gin.Context) { serveAgentStaticFile(c, paths.deployDir, "install.sh") }) r.GET("/agent/install.ps1", func(c *gin.Context) { serveAgentStaticFile(c, paths.deployDir, "install.ps1") }) for _, fileName := range agentBinaryFiles { name := fileName r.GET("/agent/"+name, func(c *gin.Context) { serveAgentStaticFile(c, paths.binDir, name) }) } r.GET("/agent/bin/:file", func(c *gin.Context) { serveAgentStaticFile(c, paths.binDir, c.Param("file")) }) r.GET("/api/agent/bin/:file", func(c *gin.Context) { serveAgentStaticFile(c, paths.binDir, c.Param("file")) }) } // resolveAgentStaticPaths 按部署形态寻找唯一 Agent 资源目录:容器优先,其次项目根目录。 // Logan.Li func resolveAgentStaticPaths(log *zap.Logger) agentStaticPaths { paths := agentStaticPaths{ binDir: firstExistingDir([]string{ "/app/agent/bin", "./agent/bin", "../agent/bin", }), deployDir: firstExistingDir([]string{ "/app/agent/deploy", "./agent/deploy", "../agent/deploy", }), } if log != nil { log.Info("Agent 静态资源目录已解析", zap.String("bin_dir", paths.binDir), zap.String("deploy_dir", paths.deployDir), ) } return paths } func firstExistingDir(candidates []string) string { for _, candidate := range candidates { info, err := os.Stat(candidate) if err == nil && info.IsDir() { return candidate } } return "" } func availableAgentDownloads(binDir string) gin.H { downloads := gin.H{} for platform, fileName := range agentBinaryFiles { if binDir == "" { continue } fullPath := filepath.Join(binDir, fileName) if info, err := os.Stat(fullPath); err == nil && !info.IsDir() { downloads[platform] = "/agent/" + fileName } } return downloads } func requestBaseURL(c *gin.Context) string { scheme := "http" if c.Request.TLS != nil { scheme = "https" } if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" { // 多级代理可能传逗号列表,首项是客户端最初使用的协议。 // Logan.Li scheme = strings.TrimSpace(strings.Split(proto, ",")[0]) } host := c.Request.Host if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" { // 多级代理可能传逗号列表,首项是客户端最初访问的域名。 // Logan.Li host = strings.TrimSpace(strings.Split(forwardedHost, ",")[0]) } if host == "" { // HTTP/1.1 正常请求必须带 Host,缺失时不再伪造本机地址。 // Logan.Li return "" } return fmt.Sprintf("%s://%s", scheme, host) } func serveAgentStaticFile(c *gin.Context, dir string, fileName string) { if dir == "" { c.JSON(http.StatusNotFound, gin.H{"error": "agent static directory not found"}) return } if strings.Contains(fileName, "/") || strings.Contains(fileName, "\\") || fileName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent file"}) return } fullPath := filepath.Join(dir, fileName) if info, err := os.Stat(fullPath); err != nil || info.IsDir() { c.JSON(http.StatusNotFound, gin.H{"error": "agent file not found"}) return } c.File(fullPath) }