- internal/lark:飞书自定义机器人交互卡片(wide_screen_mode + 彩色头部 green/red/yellow + lark_md 字段区 + 查看构建详情按钮),支持签名加签 (timestamp+sign HMAC-SHA256,密钥 enc:v1: 加密存储) - 双引擎收口:原生模拟引擎与 Jenkins 真实引擎运行结束均推送卡片; 应用/分支/构建(#N 或 runId)/状态/时间;Jenkins 构建附 externalURL 按钮直达构建详情页 - Webhook 优先级:流水线「通知 Webhook」> 全局设置(需启用) - 系统设置新增飞书通知配置区(启用开关/Webhook/签名密钥只写/ 发送测试卡片);POST /api/settings/test-lark - E2E 已验证:本地模拟机器人收到完整卡片 JSON,测试卡片正常
162 lines
5.3 KiB
Go
162 lines
5.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
|
||
|
||
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.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())
|
||
}
|
||
}
|