参考 opsx-jenkins 发布清单: - 批量发布页新增「粘贴清单」模式:多行 Job 名粘贴(#注释/内联对/ 名称版本交替)、TAG/分支切换、分支快捷 chips、700ms 防抖解析预览、 串行/并发发布;未导入的 Jenkins Job 自动创建流水线后触发 - 解析器移植 opsx-jenkins parser(参数头识别/成对校验/重名检测) 全局 AI 智能助手(右下角悬浮): - 自然语言「发布一下 cex-agent-server 等 5 个」→ 解析服务+分支+环境 → 发布计划卡片(匹配状态着色)→ 确认 → 真实触发 → runId 回显 + 跳转发布记录 - LLM 接入:OpenAI 兼容接口(DeepSeek/GLM/Qwen),系统设置页配置 API Key(加密存储)+ 测试连接;未配置时自动降级本地规则模式, 服务名/分支关键词匹配,功能不中断 - 候选池 = 流水线 + Jenkins Job(60s 缓存);精确/忽略大小写/包含 三级匹配 新端点:POST /api/ai/chat、/api/ai/release、/api/batch/parse、 POST /api/settings/test-llm;SystemSetting 扩展 LLM 字段
161 lines
5.3 KiB
Go
161 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("/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())
|
||
}
|
||
}
|