Files
FlowStack/backend/main.go
T
Logan.Li 04d1342b27 feat: FlowStack 发布系统首个可运行版本 (gin + React)
- Go gin 后端:流水线/节点/连线/运行/日志/GitOps/插件 REST API,
  SQLite(GORM) 自动建表 + seed,模拟执行引擎 + SSE 实时日志推送,
  前端产物 go:embed 嵌入单二进制,一个端口同时服务 API 与页面
- React 19 前端:画布式流水线编辑器(React Flow + Tekton 任务库)、
  GitOps 部署、批量发布、发布记录等 14 个模块页面,
  tRPC 全部替换为 REST(TanStack Query)
- 首次启动生成随机密码管理员账号(日志 + data/initial-admin-password.txt)
- run.sh 一键构建启动(默认端口 9527,支持 start/stop/restart/build/dev)
2026-10-27 02:44:10 +08:00

108 lines
3.0 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 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("/pipelines", handler.ListPipelines)
api.POST("/pipelines", handler.CreatePipeline)
api.GET("/pipelines/:id", handler.GetPipeline)
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.GET("/runs", handler.ListRuns)
api.GET("/runs/:runId", handler.GetRun)
api.GET("/runs/:runId/logs/stream", handler.StreamLogs(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)
}
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())
}
}