package main import ( "embed" "io/fs" "net/http" "os" "strings" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "flowstack/internal/database" "flowstack/internal/handler" ) // 前端产物在构建期拷贝到 static/ 并嵌入二进制(run.sh build) // //go:embed all:static var staticFS embed.FS func main() { if err := database.Init(); err != nil { panic("数据库初始化失败: " + err.Error()) } hub := handler.NewHub() r := gin.Default() // ?_clear=1 时下发 Clear-Site-Data,用于清除浏览器缓存的错误重定向等脏缓存 // (历史 bug:/index.html 曾返回 301 → ./,浏览器永久缓存后反复触发 ERR_TOO_MANY_REDIRECTS) r.Use(func(c *gin.Context) { if c.Query("_clear") != "" { c.Header("Clear-Site-Data", "\"cache\"") } c.Next() }) r.Use(cors.New(cors.Config{ AllowAllOrigins: true, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"*"}, AllowCredentials: false, })) r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok", "app": "flowstack"}) }) api := r.Group("/api") { api.POST("/auth/login", handler.Login) api.GET("/summary", handler.GetSummary) api.GET("/settings", handler.GetSettings) api.PUT("/settings", handler.UpdateSettings) api.POST("/settings/test-llm", handler.TestLLM) api.POST("/ai/chat", handler.AiChat) api.POST("/ai/release", handler.AiRelease(hub)) api.POST("/batch/parse", handler.BatchParse) api.GET("/pipelines", handler.ListPipelines) api.POST("/pipelines", handler.CreatePipeline) api.GET("/pipelines/:id", handler.GetPipelineDetail) api.PUT("/pipelines/:id", handler.UpdatePipeline) api.DELETE("/pipelines/:id", handler.DeletePipeline) api.PUT("/pipelines/:id/nodes", handler.SaveNodes) api.PUT("/pipelines/:id/edges", handler.SaveEdges) api.POST("/pipelines/:id/runs", handler.CreateRun(hub)) api.POST("/hooks/pipeline/:id", handler.InboundPipelineHook(hub)) api.GET("/runs", handler.ListRuns) api.GET("/runs/:runId", handler.GetRun) api.GET("/runs/:runId/logs/stream", handler.StreamLogs(hub)) api.POST("/runs/:runId/retry", handler.RetryRun(hub)) api.GET("/gitops/apps", handler.ListGitOpsApps) api.GET("/gitops/argo/apps", handler.ArgoList) api.POST("/gitops/argo/sync", handler.ArgoSync) api.GET("/gitops/argo/:appName/tree", handler.ArgoResourceTree) api.GET("/gitops/argo/:appName/diff", handler.ArgoDiff) api.GET("/plugins", handler.ListPlugins) api.GET("/plugins/available", handler.AvailablePlugins) api.POST("/plugins/install", handler.InstallPlugin) api.POST("/plugins/:id/toggle", handler.TogglePlugin) api.DELETE("/plugins/:id", handler.UninstallPlugin) api.GET("/hosts", handler.ListHosts) api.POST("/hosts", handler.CreateHost) api.PUT("/hosts/:id", handler.UpdateHost) api.DELETE("/hosts/:id", handler.DeleteHost) api.POST("/hosts/:id/test", handler.TestHost) api.GET("/clusters", handler.ListClusters) api.POST("/clusters", handler.CreateCluster) api.PUT("/clusters/:id", handler.UpdateCluster) api.DELETE("/clusters/:id", handler.DeleteCluster) api.POST("/clusters/:id/test", handler.TestCluster) api.GET("/credentials", handler.ListCredentials) api.POST("/credentials", handler.CreateCredential) api.PUT("/credentials/:id", handler.UpdateCredential) api.DELETE("/credentials/:id", handler.DeleteCredential) api.GET("/build-nodes", handler.ListBuildNodes) api.POST("/build-nodes", handler.CreateBuildNode) api.PUT("/build-nodes/:id", handler.UpdateBuildNode) api.DELETE("/build-nodes/:id", handler.DeleteBuildNode) api.POST("/build-nodes/:id/test", handler.TestBuildNode) api.GET("/tools", handler.ListTools) api.PUT("/tools/:id", handler.UpdateTool) api.POST("/tools/:id/check", handler.CheckTool) api.POST("/tools/:id/install", handler.InstallTool) api.GET("/webhooks", handler.ListWebhooks) api.POST("/webhooks", handler.CreateWebhook) api.PUT("/webhooks/:id", handler.UpdateWebhook) api.DELETE("/webhooks/:id", handler.DeleteWebhook) api.POST("/webhooks/:id/test", handler.TestWebhook) api.GET("/jenkins-servers", handler.ListJenkinsServers) api.POST("/jenkins-servers", handler.CreateJenkinsServer) api.PUT("/jenkins-servers/:id", handler.UpdateJenkinsServer) api.DELETE("/jenkins-servers/:id", handler.DeleteJenkinsServer) api.POST("/jenkins-servers/:id/test", handler.TestJenkinsServer) api.GET("/jenkins-servers/:id/jobs", handler.ListJenkinsJobs) api.POST("/jenkins-servers/:id/import", handler.ImportJenkinsJobs) } distFS, _ := fs.Sub(staticFS, "static") r.NoRoute(func(c *gin.Context) { path := strings.TrimPrefix(c.Request.URL.Path, "/") if path != "" { if f, err := distFS.Open(path); err == nil { f.Close() c.FileFromFS("/"+path, http.FS(distFS)) return } } // SPA 路由回退:直接返回 index.html 内容 // (不能用 FileFromFS,http.FileServer 会把 /index.html 301 到 ./ 造成死循环) index, err := fs.ReadFile(distFS, "index.html") if err != nil { c.String(http.StatusNotFound, "前端资源未构建") return } c.Data(http.StatusOK, "text/html; charset=utf-8", index) }) port := os.Getenv("PORT") if port == "" { port = "9527" } if err := r.Run(":" + port); err != nil { panic("服务启动失败: " + err.Error()) } }