package router import ( "os" "strings" "github.com/engigu/taskpool/internal/controllers" "github.com/engigu/taskpool/internal/middleware" "github.com/engigu/taskpool/internal/services" "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" ) type Controllers struct { Task *controllers.TaskController Auth *controllers.AuthController Env *controllers.EnvController Script *controllers.ScriptController Executor *controllers.ExecutorController File *controllers.FileController Dashboard *controllers.DashboardController Log *controllers.LogController LogSSE *controllers.LogSSEController Terminal *controllers.TerminalController Settings *controllers.SettingsController Dependency *controllers.DependencyController Agent *controllers.AgentController Mise *controllers.MiseController Notification *controllers.NotificationController AppLog *controllers.AppLogController SystemWS *controllers.SystemWSController WebUI *controllers.WebUIController Monitor *controllers.MonitorController Interconnect *controllers.InterconnectController Data *controllers.DataController Install *controllers.InstallController MCP *controllers.MCPController } func Setup(c *Controllers) *gin.Engine { if os.Getenv("GIN_MODE") == "" { gin.SetMode(gin.ReleaseMode) } router := gin.New() router.Use(middleware.GinLogger(), middleware.GinRecovery()) router.Use(middleware.TravelProxyMiddleware()) // 获取 URL 前缀 cfg := services.GetConfig() urlPrefix := strings.TrimSuffix(cfg.Server.URLPrefix, "/") // 创建一个路由组,如果有前缀则使用前缀,否则使用根路径 var root *gin.RouterGroup if urlPrefix != "" { root = router.Group(urlPrefix) } else { root = router.Group("") } // 按需绑定 Pprof 调试路由 (注册在 root 下以支持 URLPrefix) if cfg.Server.PprofEnabled { // pprof.RouteRegister 会在传入的路由组下注册 /debug/pprof 等路由 pprof.RouteRegister(root) } // ========================================================================= // 路由分类组装 (对应 Nginx 的 location 块分发) // ========================================================================= // 1. [ location /assets ] 静态资源路由 initStaticRoutes(root) // 3. [ location /api ] 内部 API 路由组 apiV1 := root.Group("/api/v1") initPublicAPIRoutes(apiV1, c) // 公开接口 (无需认证) initAuthorizedAPIRoutes(apiV1, c) // 授权接口 (需 JWT) // 4. [ location /api/agent ] Agent 相关 API 路由组 initAgentAPIRoutes(root, c) initOpenAPIV1Routes(root, c) // 5. [ location /mcp ] MCP Server 路由(供 AI Agent 使用) initMCPRoutes(root, c) // ========================================================================= // [ location / ] 全局 404 兜底与 SPA 渲染 // 对应 Nginx: try_files $uri $uri/ /index.html; // ========================================================================= router.NoRoute(func(ctx *gin.Context) { path := ctx.Request.URL.Path // 如果配置了前缀,只处理带前缀的路径 if urlPrefix != "" && !strings.HasPrefix(path, urlPrefix) { ctx.Status(404) return } // 解析实际的相对路径 relPath := strings.TrimPrefix(path, urlPrefix) if !strings.HasPrefix(relPath, "/") { relPath = "/" + relPath } // 拦截器:不该返回 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/") || strings.HasPrefix(relPath, "/debug/") || hasAnyExt { ctx.String(404, "404 Not Found") return } // 其他所有有效的前端页面路径(如 /tasks, /settings),都返回 index.html 交给 vue-router 处理 serveSPA(ctx, urlPrefix, 200) }) return router }