Files
FlowStack/backend/main.go
T
Logan.Li 581d0a8351 feat: Jenkins Job 批量发布 + 全局 AI 智能助手 (v0.3.0)
参考 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 字段
2026-10-27 09:07:42 +08:00

161 lines
5.3 KiB
Go
Raw Permalink 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("/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())
}
}