diff --git a/docker/docker-entrypoint-dev.sh b/docker/docker-entrypoint-dev.sh
index 063f6a4..f81f29d 100644
--- a/docker/docker-entrypoint-dev.sh
+++ b/docker/docker-entrypoint-dev.sh
@@ -82,7 +82,7 @@ export MISE_DATA_DIR="$MISE_DIR"
export MISE_CONFIG_DIR="$MISE_DIR"
export PATH="$MISE_DIR/shims:$MISE_DIR/bin:$PATH"
-export PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
+export PIP_INDEX_URL=${PIP_INDEX_URL:-https://pypi.org/simple}
log "mise version: $(mise --version 2>/dev/null | head -n 1)"
log "python: $(python --version 2>&1 | head -n 1) at $(which python)"
diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh
index 391dfa1..2c5b237 100644
--- a/docker/docker-entrypoint.sh
+++ b/docker/docker-entrypoint.sh
@@ -45,7 +45,7 @@ export PATH="$MISE_DIR/shims:$MISE_DIR/bin:$PATH"
export NODE_PATH=$(npm root -g 2>/dev/null)
# 默认启用 Python 镜像源
-export PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
+export PIP_INDEX_URL=${PIP_INDEX_URL:-https://pypi.org/simple}
# Node 内存限制
export NODE_OPTIONS="--max-old-space-size=256"
diff --git a/internal/router/api_routes.go b/internal/router/api_routes.go
new file mode 100644
index 0000000..854b06c
--- /dev/null
+++ b/internal/router/api_routes.go
@@ -0,0 +1,244 @@
+package router
+
+import (
+ // "github.com/engigu/baihu-panel/internal/controllers"
+ "github.com/engigu/baihu-panel/internal/middleware"
+
+ "github.com/gin-gonic/gin"
+)
+
+func initPublicAPIRoutes(api *gin.RouterGroup, c *Controllers) {
+ // Health check (无需认证)
+ api.GET("/ping", func(ctx *gin.Context) {
+ ctx.JSON(200, gin.H{"message": "pong"})
+ })
+
+ // Authentication routes (无需认证)
+ auth := api.Group("/auth")
+ {
+ auth.POST("/login", c.Auth.Login)
+ auth.POST("/logout", c.Auth.Logout)
+ auth.POST("/register", c.Auth.Register)
+ }
+
+ // 公开的站点设置(无需认证)
+ api.GET("/settings/public", c.Settings.GetPublicSiteSettings)
+}
+
+func initAuthorizedAPIRoutes(api *gin.RouterGroup, c *Controllers) {
+ authorized := api.Group("")
+ authorized.Use(middleware.AuthRequired())
+ {
+ // 获取当前用户
+ authorized.GET("/auth/me", c.Auth.GetCurrentUser)
+
+ // 仪表盘统计
+ authorized.GET("/stats", c.Dashboard.GetStats)
+ authorized.GET("/sentence", c.Dashboard.GetSentence)
+ authorized.GET("/sendstats", c.Dashboard.GetSendStats)
+ authorized.GET("/taskstats", c.Dashboard.GetTaskStats)
+
+ registerTaskRoutes(authorized, c)
+ registerEnvRoutes(authorized, c)
+ registerScriptRoutes(authorized, c)
+ registerFileRoutes(authorized, c)
+ registerLogRoutes(authorized, c)
+ registerTerminalRoutes(authorized, c)
+ registerSettingsRoutes(authorized, c)
+ registerDependencyRoutes(authorized, c)
+ registerAgentRoutes(authorized, c)
+ registerMiseRoutes(authorized, c)
+ registerNotificationRoutes(authorized, c)
+ registerAppLogRoutes(authorized, c)
+ }
+
+ // 通知发送 API(使用通知 Token 认证,供脚本调用)
+ notifyAPI := api.Group("/notify")
+ notifyAPI.Use(middleware.NotifyTokenAuth())
+ {
+ notifyAPI.POST("/send", c.Notification.SendNotification)
+ }
+}
+
+func registerTaskRoutes(g *gin.RouterGroup, c *Controllers) {
+ tasks := g.Group("/tasks")
+ {
+ tasks.POST("", c.Task.CreateTask)
+ tasks.GET("", c.Task.GetTasks)
+ tasks.GET("/:id", c.Task.GetTask)
+ tasks.PUT("/:id", c.Task.UpdateTask)
+ tasks.DELETE("/:id", c.Task.DeleteTask)
+ tasks.POST("/stop/:logID", c.Task.StopTask)
+ }
+
+ execution := g.Group("/execute")
+ {
+ execution.POST("/task/:id", c.Executor.ExecuteTask)
+ execution.POST("/command", c.Executor.ExecuteCommand)
+ execution.GET("/results", c.Executor.GetLastResults)
+ }
+}
+
+func registerEnvRoutes(g *gin.RouterGroup, c *Controllers) {
+ env := g.Group("/env")
+ {
+ env.POST("", c.Env.CreateEnvVar)
+ env.GET("", c.Env.GetEnvVars)
+ env.GET("/all", c.Env.GetAllEnvVars)
+ env.GET("/:id", c.Env.GetEnvVar)
+ env.GET("/:id/tasks", c.Env.GetAssociatedTasks)
+ env.PUT("/:id", c.Env.UpdateEnvVar)
+ env.DELETE("/:id", c.Env.DeleteEnvVar)
+ }
+}
+
+func registerScriptRoutes(g *gin.RouterGroup, c *Controllers) {
+ scripts := g.Group("/scripts")
+ {
+ scripts.POST("", c.Script.CreateScript)
+ scripts.GET("", c.Script.GetScripts)
+ scripts.GET("/:id", c.Script.GetScript)
+ scripts.PUT("/:id", c.Script.UpdateScript)
+ scripts.DELETE("/:id", c.Script.DeleteScript)
+ }
+}
+
+func registerFileRoutes(g *gin.RouterGroup, c *Controllers) {
+ files := g.Group("/files")
+ {
+ files.GET("/tree", c.File.GetFileTree)
+ files.GET("/content", c.File.GetFileContent)
+ files.GET("/download", c.File.DownloadFile)
+ files.POST("/content", c.File.SaveFileContent)
+ files.POST("/create", c.File.CreateFile)
+ files.POST("/delete", c.File.DeleteFile)
+ files.POST("/rename", c.File.RenameFile)
+ files.POST("/move", c.File.MoveFile)
+ files.POST("/copy", c.File.CopyFile)
+ files.POST("/upload", c.File.UploadArchive)
+ files.POST("/uploadfiles", c.File.UploadFiles)
+ }
+}
+
+func registerLogRoutes(g *gin.RouterGroup, c *Controllers) {
+ logs := g.Group("/logs")
+ {
+ logs.GET("", c.Log.GetLogs)
+ logs.POST("/clear", c.Log.ClearLogs)
+ logs.GET("/ws", c.LogWS.StreamLog)
+ logs.GET("/:id", c.Log.GetLogDetail)
+ logs.DELETE("/:id", c.Log.DeleteLog)
+ }
+}
+
+func registerTerminalRoutes(g *gin.RouterGroup, c *Controllers) {
+ g.GET("/terminal/ws", c.Terminal.HandleWebSocket)
+ // g.POST("/terminal/exec", c.Terminal.ExecuteShellCommand) // 暂未使用,已注释
+ g.GET("/terminal/cmds", c.Terminal.GetCommands)
+}
+
+func registerSettingsRoutes(g *gin.RouterGroup, c *Controllers) {
+ settings := g.Group("/settings")
+ {
+ settings.POST("/password", c.Settings.ChangePassword)
+ settings.GET("/site", c.Settings.GetSiteSettings)
+ settings.PUT("/site", c.Settings.UpdateSiteSettings)
+ settings.POST("/site/openapi-token/generate", c.Settings.GenerateOpenapiToken)
+ settings.GET("/paths", c.Settings.GetPaths)
+ settings.GET("/scheduler", c.Settings.GetSchedulerSettings)
+ settings.PUT("/scheduler", c.Settings.UpdateSchedulerSettings)
+ settings.GET("/about", c.Settings.GetAbout)
+ settings.GET("/loginlogs", c.Settings.GetLoginLogs)
+ settings.POST("/backup", c.Settings.CreateBackup)
+ settings.GET("/backup/status", c.Settings.GetBackupStatus)
+ settings.GET("/backup/download", c.Settings.DownloadBackup)
+ settings.POST("/restore", c.Settings.RestoreBackup)
+ // 通用设置接口
+ settings.GET("/:section/:key", c.Settings.GetSetting)
+ settings.POST("/:section/:key/generate", c.Settings.GenerateSettingToken)
+ }
+}
+
+func registerDependencyRoutes(g *gin.RouterGroup, c *Controllers) {
+ deps := g.Group("/deps")
+ {
+ deps.GET("", c.Dependency.List)
+ deps.POST("", c.Dependency.Create)
+ deps.DELETE("/:id", c.Dependency.Delete)
+ deps.POST("/install", c.Dependency.Install)
+ deps.POST("/install-cmd", c.Dependency.GetInstallCommand)
+ deps.POST("/uninstall/:id", c.Dependency.Uninstall)
+ deps.POST("/reinstall/:id", c.Dependency.Reinstall)
+ deps.POST("/reinstall-all", c.Dependency.ReinstallAll)
+ deps.POST("/reinstall-all-cmd", c.Dependency.GetReinstallAllCommand)
+ deps.GET("/installed", c.Dependency.GetInstalled)
+ }
+}
+
+func registerAgentRoutes(g *gin.RouterGroup, c *Controllers) {
+ agents := g.Group("/agents")
+ {
+ agents.GET("", c.Agent.List)
+ agents.GET("/version", c.Agent.GetVersion)
+ agents.PUT("/:id", c.Agent.Update)
+ agents.DELETE("/:id", c.Agent.Delete)
+ agents.POST("/:id/token", c.Agent.RegenerateToken)
+ agents.POST("/:id/update", c.Agent.ForceUpdate)
+ // 令牌管理
+ agents.GET("/tokens", c.Agent.ListTokens)
+ agents.POST("/tokens", c.Agent.CreateToken)
+ agents.DELETE("/tokens/:id", c.Agent.DeleteToken)
+ }
+
+ // Agent API(供前端调用,保持在 v1 下)
+ agentAPIv1 := g.Group("/agent")
+ {
+ agentAPIv1.GET("/download", c.Agent.Download)
+ }
+}
+
+func registerMiseRoutes(g *gin.RouterGroup, c *Controllers) {
+ mise := g.Group("/mise")
+ {
+ mise.GET("/ls", c.Mise.List)
+ mise.POST("/sync", c.Mise.Sync)
+ mise.GET("/plugins", c.Mise.Plugins)
+ mise.GET("/versions", c.Mise.Versions)
+ mise.GET("/verify-cmd", c.Mise.VerifyCommand)
+ }
+}
+
+func registerNotificationRoutes(g *gin.RouterGroup, c *Controllers) {
+ notify := g.Group("/notify")
+ {
+ notify.GET("/types", c.Notification.GetChannelTypes)
+ notify.GET("/channels", c.Notification.GetChannels)
+ notify.POST("/channels", c.Notification.SaveChannel)
+ notify.DELETE("/channels/:id", c.Notification.DeleteChannel)
+ notify.POST("/channels/test", c.Notification.TestChannel)
+ notify.GET("/bindings", c.Notification.GetBindings)
+ notify.POST("/bindings", c.Notification.SaveBinding)
+ notify.DELETE("/bindings/:id", c.Notification.DeleteBinding)
+ }
+}
+
+func registerAppLogRoutes(g *gin.RouterGroup, c *Controllers) {
+ appLogs := g.Group("/app-logs")
+ {
+ appLogs.GET("", c.AppLog.GetLogs)
+ appLogs.POST("/read", c.AppLog.MarkAsRead)
+ appLogs.POST("/clear", c.AppLog.ClearLogs)
+ }
+}
+
+func initAgentAPIRoutes(root *gin.RouterGroup, c *Controllers) {
+ // Agent API(供远程 Agent 调用,不使用 /v1 版本号)
+ agentAPI := root.Group("/api/agent")
+ {
+ agentAPI.POST("/heartbeat", c.Agent.Heartbeat)
+ agentAPI.GET("/tasks", c.Agent.GetTasks)
+ agentAPI.POST("/report", c.Agent.ReportResult)
+ agentAPI.GET("/download", c.Agent.Download) // 也在这里注册,兼容 Agent 调用
+ agentAPI.GET("/ws", c.Agent.WSConnect) // WebSocket 连接
+ }
+}
diff --git a/internal/router/openapi_routes.go b/internal/router/openapi_routes.go
new file mode 100644
index 0000000..f18d085
--- /dev/null
+++ b/internal/router/openapi_routes.go
@@ -0,0 +1,92 @@
+package router
+
+import (
+ "encoding/json"
+ "net/http"
+ "strings"
+
+ "github.com/engigu/baihu-panel/internal/constant"
+ "github.com/engigu/baihu-panel/internal/middleware"
+ "github.com/engigu/baihu-panel/internal/models/vo"
+ "github.com/engigu/baihu-panel/internal/services"
+ "github.com/engigu/baihu-panel/openapi_docs"
+
+ "github.com/gin-gonic/gin"
+)
+
+func initOpenAPIRoutes(root *gin.RouterGroup, urlPrefix string) {
+ // OpenAPI documentation using Scalar UI (带 Basic Auth 认证)
+ root.GET("/openapi/*any", func(c *gin.Context) {
+ settingsSvc := services.NewSettingsService()
+ siteConfig := settingsSvc.GetSection(constant.SectionSite)
+ tokenJson := siteConfig[constant.KeyOpenapiToken]
+
+ enabled := false
+ if tokenJson != "" {
+ var tokenConfig vo.TokenConfig
+ if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err == nil {
+ enabled = tokenConfig.Enabled
+ }
+ }
+
+ // 如果未开启文档,直接返回 404 SPA 页面
+ if !enabled {
+ serveSPA(c, urlPrefix, 404)
+ return
+ }
+
+ // 执行认证
+ middleware.SwaggerAuth()(c)
+ if c.IsAborted() {
+ // 如果认证失败(且被中间件置为 404,如密码错误且我们想要隐藏它)
+ if c.Writer.Status() == http.StatusNotFound {
+ serveSPA(c, urlPrefix, 404)
+ }
+ return
+ }
+
+ // 获取内部路径并标准化(移除前后的所有斜杠)
+ path := strings.Trim(c.Param("any"), "/")
+
+ // 1. 根路径或空路径 -> 重定向到 index.html
+ if path == "" {
+ c.Redirect(http.StatusMovedPermanently, c.Request.URL.Path+"index.html")
+ return
+ }
+
+ // 2. 提供 Scalar 渲染的 HTML 页面
+ if path == "index.html" {
+ scalarHTML := `
+
+
+ Baihu Panel API Reference
+
+
+
+
+
+
+
+
+`
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.Status(http.StatusOK)
+ c.Writer.Write([]byte(scalarHTML))
+ c.Abort()
+ return
+ }
+
+ // 3. 提供给 Scalar/Swagger 使用 host 环境变量后的 doc.json 内容
+ if path == "doc.json" {
+ doc := openapi_docs.SwaggerInfo.ReadDoc()
+ c.Header("Content-Type", "application/json; charset=utf-8")
+ c.String(http.StatusOK, doc)
+ return
+ }
+
+ // 其他未匹配路径 -> 返回 404 SPA 页面
+ serveSPA(c, urlPrefix, 404)
+ })
+}
diff --git a/internal/router/router.go b/internal/router/router.go
index 230d090..e1abf7c 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -1,18 +1,11 @@
package router
import (
- "encoding/json"
- "io/fs"
- "net/http"
"strings"
- "github.com/engigu/baihu-panel/internal/constant"
"github.com/engigu/baihu-panel/internal/controllers"
"github.com/engigu/baihu-panel/internal/middleware"
- "github.com/engigu/baihu-panel/internal/models/vo"
"github.com/engigu/baihu-panel/internal/services"
- "github.com/engigu/baihu-panel/internal/static"
- "github.com/engigu/baihu-panel/openapi_docs"
"github.com/gin-gonic/gin"
)
@@ -36,22 +29,6 @@ type Controllers struct {
AppLog *controllers.AppLogController
}
-func mustSubFS(fsys fs.FS, dir string) fs.FS {
- sub, err := fs.Sub(fsys, dir)
- if err != nil {
- panic(err)
- }
- return sub
-}
-
-// cacheControl 返回设置 Cache-Control header 的中间件
-func cacheControl(value string) gin.HandlerFunc {
- return func(c *gin.Context) {
- c.Header("Cache-Control", value)
- c.Next()
- }
-}
-
func Setup(c *Controllers) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
@@ -69,18 +46,29 @@ func Setup(c *Controllers) *gin.Engine {
root = router.Group("")
}
+ // =========================================================================
+ // 路由分类组装 (对应 Nginx 的 location 块分发)
+ // =========================================================================
+
+ // 1. [ location /assets ] 静态资源路由
initStaticRoutes(root)
+
+ // 2. [ location /openapi ] OpenAPI 文档路由
initOpenAPIRoutes(root, urlPrefix)
- // API 路由组
+ // 3. [ location /api ] 内部 API 路由组
apiV1 := root.Group("/api/v1")
- initPublicAPIRoutes(apiV1, c)
- initAuthorizedAPIRoutes(apiV1, c)
+ initPublicAPIRoutes(apiV1, c) // 公开接口 (无需认证)
+ initAuthorizedAPIRoutes(apiV1, c) // 授权接口 (需 JWT)
+
+ // 4. [ location /api/agent ] Agent 相关 API 路由组
initAgentAPIRoutes(root, c)
initOpenAPIV1Routes(root, c)
- // SPA 兜底路由 - 返回 index.html(HTML禁用缓存以保证实时同步)
- // 必须在最后注册,作为兜底路由
+ // =========================================================================
+ // [ location / ] 全局 404 兜底与 SPA 渲染
+ // 对应 Nginx: try_files $uri $uri/ /index.html;
+ // =========================================================================
router.NoRoute(func(ctx *gin.Context) {
path := ctx.Request.URL.Path
@@ -96,389 +84,23 @@ func Setup(c *Controllers) *gin.Engine {
relPath = "/" + relPath
}
- // 拦截属于 API 或静态资源目录下不存在的请求,绝不能返回 index.html 造成前端 MIME 错误
- if strings.HasPrefix(relPath, "/api/") || strings.HasPrefix(relPath, "/assets/") {
- ctx.String(404, "Not Found")
+ // 拦截器:不该返回 index.html 的情况
+ // 如果该请求被识别为 API 请求、静态资源请求,或者是带有明确文件后缀(如 .js / .css / .png)的物理文件请求
+ // 都不应该返回 SPA 页面(会报前端 MIME 类型错误),而是直接掐断返回 404
+ hasAnyExt := false
+ if idx := strings.LastIndex(relPath, "."); idx > 0 && len(relPath)-idx < 6 {
+ // 简单判断是否有后缀(如 .js, .css)
+ hasAnyExt = true
+ }
+
+ if strings.HasPrefix(relPath, "/api/") || strings.HasPrefix(relPath, "/assets/") || hasAnyExt {
+ ctx.String(404, "404 Not Found")
return
}
+ // 其他所有有效的前端页面路径(如 /tasks, /settings),都返回 index.html 交给 vue-router 处理
serveSPA(ctx, urlPrefix, 200)
})
return router
}
-
-func initStaticRoutes(root *gin.RouterGroup) {
- staticFS := static.GetFS()
- if staticFS == nil {
- return
- }
-
- // 静态资源服务(Vue SPA),带缓存头部
- assetsGroup := root.Group("/assets")
- assetsGroup.Use(cacheControl("public, max-age=31536000, immutable")) // 带哈希的资源缓存
- assetsGroup.StaticFS("/", http.FS(mustSubFS(staticFS, "assets")))
-
- // logo.svg 短缓存实现
- root.GET("/logo.svg", func(ctx *gin.Context) {
- data, err := static.ReadFile("logo.svg")
- if err != nil {
- ctx.Status(404)
- return
- }
- ctx.Header("Cache-Control", "public, max-age=86400") // 缓存1天
- ctx.Data(200, "image/svg+xml", data)
- })
-}
-
-func initOpenAPIRoutes(root *gin.RouterGroup, urlPrefix string) {
- // OpenAPI documentation using Scalar UI (带 Basic Auth 认证)
- root.GET("/openapi/*any", func(c *gin.Context) {
- settingsSvc := services.NewSettingsService()
- siteConfig := settingsSvc.GetSection(constant.SectionSite)
- tokenJson := siteConfig[constant.KeyOpenapiToken]
-
- enabled := false
- if tokenJson != "" {
- var tokenConfig vo.TokenConfig
- if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err == nil {
- enabled = tokenConfig.Enabled
- }
- }
-
- // 如果未开启文档,直接返回 404 SPA 页面
- if !enabled {
- serveSPA(c, urlPrefix, 404)
- return
- }
-
- // 执行认证
- middleware.SwaggerAuth()(c)
- if c.IsAborted() {
- // 如果认证失败(且被中间件置为 404,如密码错误且我们想要隐藏它)
- if c.Writer.Status() == http.StatusNotFound {
- serveSPA(c, urlPrefix, 404)
- }
- return
- }
-
- // 获取内部路径并标准化(移除前后的所有斜杠)
- path := strings.Trim(c.Param("any"), "/")
-
- // 1. 根路径或空路径 -> 重定向到 index.html
- if path == "" {
- c.Redirect(http.StatusMovedPermanently, c.Request.URL.Path+"index.html")
- return
- }
-
- // 2. 提供 Scalar 渲染的 HTML 页面
- if path == "index.html" {
- scalarHTML := `
-
-
- Baihu Panel API Reference
-
-
-
-
-
-
-
-
-`
- c.Header("Content-Type", "text/html; charset=utf-8")
- c.Status(http.StatusOK)
- c.Writer.Write([]byte(scalarHTML))
- c.Abort()
- return
- }
-
- // 3. 提供给 Scalar/Swagger 使用 host 环境变量后的 doc.json 内容
- if path == "doc.json" {
- doc := openapi_docs.SwaggerInfo.ReadDoc()
- c.Header("Content-Type", "application/json; charset=utf-8")
- c.String(http.StatusOK, doc)
- return
- }
-
- // 其他未匹配路径 -> 返回 404 SPA 页面
- serveSPA(c, urlPrefix, 404)
- })
-}
-
-func initPublicAPIRoutes(api *gin.RouterGroup, c *Controllers) {
- // Health check (无需认证)
- api.GET("/ping", func(ctx *gin.Context) {
- ctx.JSON(200, gin.H{"message": "pong"})
- })
-
- // Authentication routes (无需认证)
- auth := api.Group("/auth")
- {
- auth.POST("/login", c.Auth.Login)
- auth.POST("/logout", c.Auth.Logout)
- auth.POST("/register", c.Auth.Register)
- }
-
- // 公开的站点设置(无需认证)
- api.GET("/settings/public", c.Settings.GetPublicSiteSettings)
-}
-
-func initAuthorizedAPIRoutes(api *gin.RouterGroup, c *Controllers) {
- authorized := api.Group("")
- authorized.Use(middleware.AuthRequired())
- {
- // 获取当前用户
- authorized.GET("/auth/me", c.Auth.GetCurrentUser)
-
- // 仪表盘统计
- authorized.GET("/stats", c.Dashboard.GetStats)
- authorized.GET("/sentence", c.Dashboard.GetSentence)
- authorized.GET("/sendstats", c.Dashboard.GetSendStats)
- authorized.GET("/taskstats", c.Dashboard.GetTaskStats)
-
- registerTaskRoutes(authorized, c)
- registerEnvRoutes(authorized, c)
- registerScriptRoutes(authorized, c)
- registerFileRoutes(authorized, c)
- registerLogRoutes(authorized, c)
- registerTerminalRoutes(authorized, c)
- registerSettingsRoutes(authorized, c)
- registerDependencyRoutes(authorized, c)
- registerAgentRoutes(authorized, c)
- registerMiseRoutes(authorized, c)
- registerNotificationRoutes(authorized, c)
- registerAppLogRoutes(authorized, c)
- }
-
- // 通知发送 API(使用通知 Token 认证,供脚本调用)
- notifyAPI := api.Group("/notify")
- notifyAPI.Use(middleware.NotifyTokenAuth())
- {
- notifyAPI.POST("/send", c.Notification.SendNotification)
- }
-}
-
-func registerTaskRoutes(g *gin.RouterGroup, c *Controllers) {
- tasks := g.Group("/tasks")
- {
- tasks.POST("", c.Task.CreateTask)
- tasks.GET("", c.Task.GetTasks)
- tasks.GET("/:id", c.Task.GetTask)
- tasks.PUT("/:id", c.Task.UpdateTask)
- tasks.DELETE("/:id", c.Task.DeleteTask)
- tasks.POST("/stop/:logID", c.Task.StopTask)
- }
-
- execution := g.Group("/execute")
- {
- execution.POST("/task/:id", c.Executor.ExecuteTask)
- execution.POST("/command", c.Executor.ExecuteCommand)
- execution.GET("/results", c.Executor.GetLastResults)
- }
-}
-
-func registerEnvRoutes(g *gin.RouterGroup, c *Controllers) {
- env := g.Group("/env")
- {
- env.POST("", c.Env.CreateEnvVar)
- env.GET("", c.Env.GetEnvVars)
- env.GET("/all", c.Env.GetAllEnvVars)
- env.GET("/:id", c.Env.GetEnvVar)
- env.GET("/:id/tasks", c.Env.GetAssociatedTasks)
- env.PUT("/:id", c.Env.UpdateEnvVar)
- env.DELETE("/:id", c.Env.DeleteEnvVar)
- }
-}
-
-func registerScriptRoutes(g *gin.RouterGroup, c *Controllers) {
- scripts := g.Group("/scripts")
- {
- scripts.POST("", c.Script.CreateScript)
- scripts.GET("", c.Script.GetScripts)
- scripts.GET("/:id", c.Script.GetScript)
- scripts.PUT("/:id", c.Script.UpdateScript)
- scripts.DELETE("/:id", c.Script.DeleteScript)
- }
-}
-
-func registerFileRoutes(g *gin.RouterGroup, c *Controllers) {
- files := g.Group("/files")
- {
- files.GET("/tree", c.File.GetFileTree)
- files.GET("/content", c.File.GetFileContent)
- files.GET("/download", c.File.DownloadFile)
- files.POST("/content", c.File.SaveFileContent)
- files.POST("/create", c.File.CreateFile)
- files.POST("/delete", c.File.DeleteFile)
- files.POST("/rename", c.File.RenameFile)
- files.POST("/move", c.File.MoveFile)
- files.POST("/copy", c.File.CopyFile)
- files.POST("/upload", c.File.UploadArchive)
- files.POST("/uploadfiles", c.File.UploadFiles)
- }
-}
-
-func registerLogRoutes(g *gin.RouterGroup, c *Controllers) {
- logs := g.Group("/logs")
- {
- logs.GET("", c.Log.GetLogs)
- logs.POST("/clear", c.Log.ClearLogs)
- logs.GET("/ws", c.LogWS.StreamLog)
- logs.GET("/:id", c.Log.GetLogDetail)
- logs.DELETE("/:id", c.Log.DeleteLog)
- }
-}
-
-func registerTerminalRoutes(g *gin.RouterGroup, c *Controllers) {
- g.GET("/terminal/ws", c.Terminal.HandleWebSocket)
- // g.POST("/terminal/exec", c.Terminal.ExecuteShellCommand) // 暂未使用,已注释
- g.GET("/terminal/cmds", c.Terminal.GetCommands)
-}
-
-func registerSettingsRoutes(g *gin.RouterGroup, c *Controllers) {
- settings := g.Group("/settings")
- {
- settings.POST("/password", c.Settings.ChangePassword)
- settings.GET("/site", c.Settings.GetSiteSettings)
- settings.PUT("/site", c.Settings.UpdateSiteSettings)
- settings.POST("/site/openapi-token/generate", c.Settings.GenerateOpenapiToken)
- settings.GET("/paths", c.Settings.GetPaths)
- settings.GET("/scheduler", c.Settings.GetSchedulerSettings)
- settings.PUT("/scheduler", c.Settings.UpdateSchedulerSettings)
- settings.GET("/about", c.Settings.GetAbout)
- settings.GET("/loginlogs", c.Settings.GetLoginLogs)
- settings.POST("/backup", c.Settings.CreateBackup)
- settings.GET("/backup/status", c.Settings.GetBackupStatus)
- settings.GET("/backup/download", c.Settings.DownloadBackup)
- settings.POST("/restore", c.Settings.RestoreBackup)
- // 通用设置接口
- settings.GET("/:section/:key", c.Settings.GetSetting)
- settings.POST("/:section/:key/generate", c.Settings.GenerateSettingToken)
- }
-}
-
-func registerDependencyRoutes(g *gin.RouterGroup, c *Controllers) {
- deps := g.Group("/deps")
- {
- deps.GET("", c.Dependency.List)
- deps.POST("", c.Dependency.Create)
- deps.DELETE("/:id", c.Dependency.Delete)
- deps.POST("/install", c.Dependency.Install)
- deps.POST("/install-cmd", c.Dependency.GetInstallCommand)
- deps.POST("/uninstall/:id", c.Dependency.Uninstall)
- deps.POST("/reinstall/:id", c.Dependency.Reinstall)
- deps.POST("/reinstall-all", c.Dependency.ReinstallAll)
- deps.POST("/reinstall-all-cmd", c.Dependency.GetReinstallAllCommand)
- deps.GET("/installed", c.Dependency.GetInstalled)
- }
-}
-
-func registerAgentRoutes(g *gin.RouterGroup, c *Controllers) {
- agents := g.Group("/agents")
- {
- agents.GET("", c.Agent.List)
- agents.GET("/version", c.Agent.GetVersion)
- agents.PUT("/:id", c.Agent.Update)
- agents.DELETE("/:id", c.Agent.Delete)
- agents.POST("/:id/token", c.Agent.RegenerateToken)
- agents.POST("/:id/update", c.Agent.ForceUpdate)
- // 令牌管理
- agents.GET("/tokens", c.Agent.ListTokens)
- agents.POST("/tokens", c.Agent.CreateToken)
- agents.DELETE("/tokens/:id", c.Agent.DeleteToken)
- }
-
- // Agent API(供前端调用,保持在 v1 下)
- agentAPIv1 := g.Group("/agent")
- {
- agentAPIv1.GET("/download", c.Agent.Download)
- }
-}
-
-func registerMiseRoutes(g *gin.RouterGroup, c *Controllers) {
- mise := g.Group("/mise")
- {
- mise.GET("/ls", c.Mise.List)
- mise.POST("/sync", c.Mise.Sync)
- mise.GET("/plugins", c.Mise.Plugins)
- mise.GET("/versions", c.Mise.Versions)
- mise.GET("/verify-cmd", c.Mise.VerifyCommand)
- }
-}
-
-func registerNotificationRoutes(g *gin.RouterGroup, c *Controllers) {
- notify := g.Group("/notify")
- {
- notify.GET("/types", c.Notification.GetChannelTypes)
- notify.GET("/channels", c.Notification.GetChannels)
- notify.POST("/channels", c.Notification.SaveChannel)
- notify.DELETE("/channels/:id", c.Notification.DeleteChannel)
- notify.POST("/channels/test", c.Notification.TestChannel)
- notify.GET("/bindings", c.Notification.GetBindings)
- notify.POST("/bindings", c.Notification.SaveBinding)
- notify.DELETE("/bindings/:id", c.Notification.DeleteBinding)
- }
-}
-
-func registerAppLogRoutes(g *gin.RouterGroup, c *Controllers) {
- appLogs := g.Group("/app-logs")
- {
- appLogs.GET("", c.AppLog.GetLogs)
- appLogs.POST("/read", c.AppLog.MarkAsRead)
- appLogs.POST("/clear", c.AppLog.ClearLogs)
- }
-}
-
-func initAgentAPIRoutes(root *gin.RouterGroup, c *Controllers) {
- // Agent API(供远程 Agent 调用,不使用 /v1 版本号)
- agentAPI := root.Group("/api/agent")
- {
- agentAPI.POST("/heartbeat", c.Agent.Heartbeat)
- agentAPI.GET("/tasks", c.Agent.GetTasks)
- agentAPI.POST("/report", c.Agent.ReportResult)
- agentAPI.GET("/download", c.Agent.Download) // 也在这里注册,兼容 Agent 调用
- agentAPI.GET("/ws", c.Agent.WSConnect) // WebSocket 连接
- }
-}
-
-// serveSPA 注入配置并返回 index.html 给前端渲染
-func serveSPA(ctx *gin.Context, urlPrefix string, status int) {
- data, err := static.ReadFile("index.html")
- if err != nil {
- // 如果读不到 index.html (如 dev 模式未 build),返回基础 HTML
- // 如果已经是 /404 路径,则不再重定向以免死循环
- path := ctx.Request.URL.Path
- if strings.HasSuffix(path, "/404") {
- ctx.Data(status, "text/html; charset=utf-8", []byte("404 Not Found
Frontend assets not found. Please run 'npm run build' or check dev server.
Go Home"))
- ctx.Abort()
- return
- }
-
- fallback := `404 Not Found
-
- Not Found. Redirecting...
- `
- ctx.Header("Content-Type", "text/html; charset=utf-8")
- ctx.Data(status, "text/html", []byte(fallback))
- ctx.Abort()
- return
- }
-
- html := string(data)
-
- // 注入配置变量供前端使用(API 调用和路由)
- configScript := ``
- html = strings.Replace(html, "", configScript+"", 1)
-
- ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
- ctx.Data(status, "text/html; charset=utf-8", []byte(html))
- ctx.Abort()
-}
diff --git a/internal/router/static_routes.go b/internal/router/static_routes.go
new file mode 100644
index 0000000..b3c0c1c
--- /dev/null
+++ b/internal/router/static_routes.go
@@ -0,0 +1,97 @@
+package router
+
+import (
+ "io/fs"
+ "net/http"
+ "strings"
+
+ "github.com/engigu/baihu-panel/internal/static"
+
+ "github.com/gin-gonic/gin"
+)
+
+func mustSubFS(fsys fs.FS, dir string) fs.FS {
+ sub, err := fs.Sub(fsys, dir)
+ if err != nil {
+ panic(err)
+ }
+ return sub
+}
+
+// cacheControl 返回设置 Cache-Control header 的中间件
+func cacheControl(value string) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Header("Cache-Control", value)
+ c.Next()
+ }
+}
+
+func initStaticRoutes(root *gin.RouterGroup) {
+ staticFS := static.GetFS()
+ if staticFS == nil {
+ return
+ }
+
+ // 静态资源服务(Vue SPA),带缓存头部
+ assetsGroup := root.Group("/assets")
+ assetsGroup.Use(cacheControl("public, max-age=31536000, immutable")) // 带哈希的资源缓存
+ assetsGroup.StaticFS("/", http.FS(mustSubFS(staticFS, "assets")))
+
+ // logo.svg 短缓存实现
+ root.GET("/logo.svg", func(ctx *gin.Context) {
+ data, err := static.ReadFile("logo.svg")
+ if err != nil {
+ ctx.Status(404)
+ return
+ }
+ ctx.Header("Cache-Control", "public, max-age=86400") // 缓存1天
+ ctx.Data(200, "image/svg+xml", data)
+ })
+}
+
+// serveSPA 注入配置并返回 index.html 给前端渲染
+func serveSPA(ctx *gin.Context, urlPrefix string, status int) {
+ data, err := static.ReadFile("index.html")
+ if err != nil {
+ // 如果读不到 index.html (如 dev 模式未 build),返回基础 HTML
+ // 如果已经是 /404 路径,则不再重定向以免死循环
+ path := ctx.Request.URL.Path
+ if strings.HasSuffix(path, "/404") {
+ ctx.Data(status, "text/html; charset=utf-8", []byte("404 Not Found
Frontend assets not found. Please run 'npm run build' or check dev server.
Go Home"))
+ ctx.Abort()
+ return
+ }
+
+ fallback := `404 Not Found
+
+ Not Found. Redirecting...
+ `
+ ctx.Header("Content-Type", "text/html; charset=utf-8")
+ ctx.Data(status, "text/html", []byte(fallback))
+ ctx.Abort()
+ return
+ }
+
+ html := string(data)
+
+ // 注入配置变量供前端使用(API 调用和路由)
+ baseHref := urlPrefix + "/"
+ if urlPrefix == "" {
+ baseHref = "/"
+ }
+
+ // 注入 base tag 为了让深度路由的相对路径资源加载正常
+ html = strings.Replace(html, "", "\n ", 1)
+
+ configScript := ``
+ html = strings.Replace(html, "", configScript+"", 1)
+
+ ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
+ ctx.Data(status, "text/html; charset=utf-8", []byte(html))
+ ctx.Abort()
+}
diff --git a/openapi_docs/docs.go b/openapi_docs/docs.go
index 03c3468..fc61cb6 100644
--- a/openapi_docs/docs.go
+++ b/openapi_docs/docs.go
@@ -24,6 +24,147 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
+ "/app-logs": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "应用日志"
+ ],
+ "summary": "获取应用日志列表",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "日志分类",
+ "name": "category",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "状态",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "级别",
+ "name": "level",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "搜索关键词",
+ "name": "keyword",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "页码",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量",
+ "name": "page_size",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response"
+ }
+ }
+ }
+ }
+ },
+ "/app-logs/clear": {
+ "post": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "应用日志"
+ ],
+ "summary": "清理日志",
+ "parameters": [
+ {
+ "description": "请求体",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response"
+ }
+ }
+ }
+ }
+ },
+ "/app-logs/read": {
+ "post": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "应用日志"
+ ],
+ "summary": "标记已读",
+ "parameters": [
+ {
+ "description": "请求体",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response"
+ }
+ }
+ }
+ }
+ },
"/env": {
"get": {
"security": [
diff --git a/openapi_docs/swagger.json b/openapi_docs/swagger.json
index c1fe139..0bb7a1b 100644
--- a/openapi_docs/swagger.json
+++ b/openapi_docs/swagger.json
@@ -18,6 +18,147 @@
"host": "localhost:8052",
"basePath": "/open2api/v1",
"paths": {
+ "/app-logs": {
+ "get": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "应用日志"
+ ],
+ "summary": "获取应用日志列表",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "日志分类",
+ "name": "category",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "状态",
+ "name": "status",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "级别",
+ "name": "level",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "搜索关键词",
+ "name": "keyword",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "页码",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "每页数量",
+ "name": "page_size",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response"
+ }
+ }
+ }
+ }
+ },
+ "/app-logs/clear": {
+ "post": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "应用日志"
+ ],
+ "summary": "清理日志",
+ "parameters": [
+ {
+ "description": "请求体",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response"
+ }
+ }
+ }
+ }
+ },
+ "/app-logs/read": {
+ "post": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "应用日志"
+ ],
+ "summary": "标记已读",
+ "parameters": [
+ {
+ "description": "请求体",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_engigu_baihu-panel_internal_utils.Response"
+ }
+ }
+ }
+ }
+ },
"/env": {
"get": {
"security": [
diff --git a/openapi_docs/swagger.yaml b/openapi_docs/swagger.yaml
index 5e1542f..e352e85 100644
--- a/openapi_docs/swagger.yaml
+++ b/openapi_docs/swagger.yaml
@@ -154,6 +154,93 @@ info:
title: Baihu Panel API
version: "1.0"
paths:
+ /app-logs:
+ get:
+ consumes:
+ - application/json
+ parameters:
+ - description: 日志分类
+ in: query
+ name: category
+ type: string
+ - description: 状态
+ in: query
+ name: status
+ type: string
+ - description: 级别
+ in: query
+ name: level
+ type: string
+ - description: 搜索关键词
+ in: query
+ name: keyword
+ type: string
+ - description: 页码
+ in: query
+ name: page
+ type: integer
+ - description: 每页数量
+ in: query
+ name: page_size
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response'
+ security:
+ - BearerAuth: []
+ summary: 获取应用日志列表
+ tags:
+ - 应用日志
+ /app-logs/clear:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 请求体
+ in: body
+ name: body
+ required: true
+ schema:
+ type: object
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response'
+ security:
+ - BearerAuth: []
+ summary: 清理日志
+ tags:
+ - 应用日志
+ /app-logs/read:
+ post:
+ consumes:
+ - application/json
+ parameters:
+ - description: 请求体
+ in: body
+ name: body
+ required: true
+ schema:
+ type: object
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/github_com_engigu_baihu-panel_internal_utils.Response'
+ security:
+ - BearerAuth: []
+ summary: 标记已读
+ tags:
+ - 应用日志
/env:
get:
consumes: