Files
FlowStack/backend/main.go
T
Logan.Li 555934cc64 feat: 构建节点磁盘性能测试(dd 实测读写)
- 新增 internal/sshexec(x/crypto/ssh):远程命令通道,凭证来自
  Credential(type=ssh) 解密(私钥/密码/端口/用户)
- POST /api/build-nodes/:id/disk-test:写 dd(256MB, oflag=direct,
  不支持自动降级含缓存并标注) + 读 dd + 清理;IP 为本机时直接本地
  exec;输出解析兼容 Linux/Mac 两种 dd 格式;60s 超时保护
- BuildNode 新增 credentialId 关联 SSH 凭证 + 磁盘测速结果持久字段
  (显式 column 标签修正 GORM MBps 命名)
- 节点卡片新增仪表盘按钮触发测试;磁盘行显示 ↑写 ↓读 MB/s
  (>200 绿 SSD 级 / 50-200 黄 / <50 红),hover 看测试模式与时间
- 编辑弹窗新增 SSH 凭证下拉
- 本机实测:local-mac 节点 写 2336 MB/s · 读 1793 MB/s(direct)
2026-10-27 12:21:39 +08:00

163 lines
5.4 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("/summary", handler.GetSummary)
api.GET("/settings", handler.GetSettings)
api.PUT("/settings", handler.UpdateSettings)
api.POST("/settings/test-llm", handler.TestLLM)
api.POST("/settings/test-lark", handler.TestLark)
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.POST("/build-nodes/:id/disk-test", handler.TestBuildNodeDisk)
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())
}
}