- backend/docs/GUIDE.md:使用指南,每功能一句话讲清 「是什么→怎么用→实用技巧」(快速开始 3 步、流水线快速上手配置、 GitOps 概念与用法、AI 助手/批量发布/Jenkins/构建节点/工具/ 飞书通知/用户 LDAP/常见问题 FAQ) - backend/docs/API.md:接口文档(opsx-sec 骨架:通用约定/模块 索引表/重点端点 curl 示例/状态码速查) - GET /api/docs:文档 embed 进二进制,运行目录 docs/ 优先可热更新 - /docs 页面:使用指南/接口文档切换 + 左侧目录锚点导航 + markdown 渲染(marked + gfm-heading-id,深浅主题适配排版) - 侧边栏系统组新增「文档」入口(第一位)
187 lines
6.3 KiB
Go
187 lines
6.3 KiB
Go
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
|
||
|
||
// 使用指南与接口文档(运行目录 docs/ 优先)
|
||
//
|
||
//go:embed docs/GUIDE.md docs/API.md
|
||
var docsFS embed.FS
|
||
|
||
func main() {
|
||
if err := database.Init(); err != nil {
|
||
panic("数据库初始化失败: " + err.Error())
|
||
}
|
||
|
||
hub := handler.NewHub()
|
||
r := gin.Default()
|
||
|
||
// 会话鉴权:所有 /api 需登录(豁免登录/健康检查/入站 Webhook/SSE 日志流)
|
||
r.Use(handler.AuthMiddleware())
|
||
|
||
// ?_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.POST("/auth/logout", func(c *gin.Context) { c.JSON(200, gin.H{"success": true}) })
|
||
|
||
// 用户管理(管理员)
|
||
users := api.Group("/users", handler.AdminOnly())
|
||
{
|
||
users.GET("", handler.ListUsers)
|
||
users.POST("", handler.CreateUser)
|
||
users.PUT("/:id", handler.UpdateUser)
|
||
users.DELETE("/:id", handler.DeleteUser)
|
||
users.POST("/:id/reset-password", handler.ResetPassword)
|
||
users.POST("/ldap-import", handler.LdapImport)
|
||
}
|
||
api.POST("/ldap/users", handler.AdminOnly(), handler.LdapListUsers)
|
||
api.POST("/settings/ldap/test", handler.AdminOnly(), handler.TestLDAP)
|
||
api.GET("/docs", handler.GetDocs(docsFS))
|
||
|
||
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/chat/stream", handler.AiChatStream)
|
||
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())
|
||
}
|
||
}
|