2026-04-09 10:01:23 +08:00
|
|
|
package bootstrap
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-05-31 18:51:22 +08:00
|
|
|
"agent-desk/internal/ai/mcps"
|
|
|
|
|
_ "agent-desk/internal/ai/runtime"
|
|
|
|
|
"agent-desk/internal/middleware"
|
|
|
|
|
"agent-desk/internal/pkg/config"
|
|
|
|
|
"agent-desk/internal/pkg/ginx"
|
|
|
|
|
"agent-desk/internal/pkg/httpx"
|
|
|
|
|
"agent-desk/internal/pkg/i18nx"
|
|
|
|
|
"agent-desk/internal/pkg/tracex"
|
|
|
|
|
"agent-desk/internal/services"
|
|
|
|
|
webspa "agent-desk/web"
|
2026-04-09 10:01:23 +08:00
|
|
|
|
2026-05-23 22:10:20 +08:00
|
|
|
"github.com/gin-gonic/gin"
|
2026-05-23 22:44:06 +08:00
|
|
|
"github.com/mlogclub/simple/web"
|
2026-04-09 10:01:23 +08:00
|
|
|
|
2026-05-31 18:51:22 +08:00
|
|
|
_ "agent-desk/internal/services/wx_callback_handlers"
|
2026-04-09 10:01:23 +08:00
|
|
|
)
|
|
|
|
|
|
2026-05-23 22:10:20 +08:00
|
|
|
func NewServer() (*gin.Engine, error) {
|
2026-04-09 10:01:23 +08:00
|
|
|
cfg := config.Current()
|
2026-05-24 20:10:20 +08:00
|
|
|
|
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
2026-05-24 20:13:18 +08:00
|
|
|
printBanner()
|
2026-05-24 20:10:20 +08:00
|
|
|
|
2026-05-23 22:10:20 +08:00
|
|
|
app := gin.New()
|
2026-05-27 22:18:43 +08:00
|
|
|
app.Use(requestIDMiddleware())
|
2026-05-27 22:06:32 +08:00
|
|
|
app.Use(corsMiddleware())
|
2026-05-23 22:10:20 +08:00
|
|
|
app.Use(gin.Recovery())
|
|
|
|
|
app.Use(requestLogMiddleware())
|
2026-05-27 22:06:32 +08:00
|
|
|
app.Use(maxBodySizeMiddleware())
|
2026-05-25 12:06:15 +08:00
|
|
|
app.Use(i18nx.Middleware())
|
2026-05-23 22:10:20 +08:00
|
|
|
|
|
|
|
|
addRouter(app)
|
|
|
|
|
|
2026-05-23 22:44:06 +08:00
|
|
|
notFoundPrefixes := []string{"/api/"}
|
|
|
|
|
if baseURL := strings.TrimRight(cfg.Storage.Local.BaseURL, "/"); baseURL != "" {
|
|
|
|
|
notFoundPrefixes = append(notFoundPrefixes, baseURL+"/")
|
|
|
|
|
}
|
|
|
|
|
app.StaticFS(cfg.Storage.Local.BaseURL, ginx.StaticFiles(cfg.Storage.Local.Root))
|
|
|
|
|
ginx.HandleSPA(app, ginx.SPAOptions{
|
|
|
|
|
Root: "./web/out",
|
|
|
|
|
EmbeddedFS: webspa.SPA,
|
|
|
|
|
EmbeddedRoot: "out",
|
|
|
|
|
DirOptions: ginx.DirOptions{
|
|
|
|
|
ShowList: false,
|
|
|
|
|
SPA: true,
|
|
|
|
|
IndexName: "index.html",
|
|
|
|
|
},
|
|
|
|
|
NotFoundPrefixes: notFoundPrefixes,
|
|
|
|
|
NotFoundHandler: func(ctx *gin.Context) {
|
2026-06-02 20:51:13 +08:00
|
|
|
httpx.WriteHttpStatusJSON(ctx, http.StatusNotFound, web.JsonErrorCode(http.StatusNotFound, i18nx.T(ctx, "error.notFound")))
|
2026-05-23 22:44:06 +08:00
|
|
|
},
|
|
|
|
|
})
|
2026-05-23 22:10:20 +08:00
|
|
|
|
|
|
|
|
return app, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 22:06:32 +08:00
|
|
|
func corsMiddleware() gin.HandlerFunc {
|
|
|
|
|
allowedOrigins := config.Current().Server.CORS.AllowedOrigins
|
2026-05-23 22:10:20 +08:00
|
|
|
allowHeaders := "Origin, Content-Type, Accept, Authorization, X-Requested-With, X-Guest-Id, X-Channel-Id, X-External-Id, X-External-Name, X-Customer-Session-Token, X-Customer-Session-Expires-At"
|
|
|
|
|
exposeHeaders := "Content-Length, Content-Type, Authorization, X-Guest-Id, X-Channel-Id, X-External-Id, X-External-Name, X-Customer-Session-Token, X-Customer-Session-Expires-At"
|
2026-05-27 22:05:02 +08:00
|
|
|
allowMethods := "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
|
|
|
|
allowedOriginSet := make(map[string]struct{}, len(allowedOrigins))
|
|
|
|
|
for _, origin := range allowedOrigins {
|
|
|
|
|
origin = strings.TrimRight(strings.TrimSpace(origin), "/")
|
|
|
|
|
if origin == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
allowedOriginSet[origin] = struct{}{}
|
|
|
|
|
}
|
2026-05-23 22:10:20 +08:00
|
|
|
return func(ctx *gin.Context) {
|
2026-04-09 10:01:23 +08:00
|
|
|
if isWebsocketUpgrade(ctx) {
|
|
|
|
|
ctx.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-27 22:05:02 +08:00
|
|
|
origin := strings.TrimRight(strings.TrimSpace(ctx.GetHeader("Origin")), "/")
|
|
|
|
|
if origin != "" {
|
|
|
|
|
ctx.Header("Vary", "Origin")
|
|
|
|
|
if _, ok := allowedOriginSet[origin]; !ok {
|
|
|
|
|
if ctx.Request.Method == http.MethodOptions {
|
|
|
|
|
ctx.AbortWithStatus(http.StatusForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.Header("Access-Control-Allow-Origin", origin)
|
|
|
|
|
ctx.Header("Access-Control-Allow-Methods", allowMethods)
|
|
|
|
|
ctx.Header("Access-Control-Allow-Headers", allowHeaders)
|
|
|
|
|
ctx.Header("Access-Control-Expose-Headers", exposeHeaders)
|
|
|
|
|
ctx.Header("Access-Control-Max-Age", "600")
|
|
|
|
|
}
|
2026-05-23 22:10:20 +08:00
|
|
|
if ctx.Request.Method == http.MethodOptions {
|
|
|
|
|
ctx.AbortWithStatus(http.StatusNoContent)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.Next()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 22:18:43 +08:00
|
|
|
func requestIDMiddleware() gin.HandlerFunc {
|
|
|
|
|
return func(ctx *gin.Context) {
|
|
|
|
|
requestID := tracex.EnsureRequestID(ctx.GetHeader(tracex.RequestIDHeader))
|
|
|
|
|
ctx.Set(tracex.GinRequestIDKey, requestID)
|
|
|
|
|
if requestID != "" {
|
|
|
|
|
ctx.Header(tracex.RequestIDHeader, requestID)
|
|
|
|
|
}
|
|
|
|
|
ctx.Next()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 22:10:20 +08:00
|
|
|
func requestLogMiddleware() gin.HandlerFunc {
|
|
|
|
|
return func(ctx *gin.Context) {
|
2026-04-09 10:01:23 +08:00
|
|
|
start := time.Now()
|
2026-05-23 22:10:20 +08:00
|
|
|
path := ctx.Request.URL.Path
|
|
|
|
|
method := ctx.Request.Method
|
2026-05-27 22:18:43 +08:00
|
|
|
requestID, _ := ctx.Get(tracex.GinRequestIDKey)
|
2026-04-09 10:01:23 +08:00
|
|
|
ctx.Next()
|
|
|
|
|
|
|
|
|
|
slog.Info("http request",
|
2026-05-27 22:18:43 +08:00
|
|
|
"requestId", requestID,
|
2026-04-09 10:01:23 +08:00
|
|
|
"method", method,
|
|
|
|
|
"path", path,
|
2026-05-23 22:10:20 +08:00
|
|
|
"status", ctx.Writer.Status(),
|
2026-04-09 10:01:23 +08:00
|
|
|
"elapsed", time.Since(start).Milliseconds(),
|
2026-05-23 22:10:20 +08:00
|
|
|
"clientIp", ctx.ClientIP(),
|
2026-04-09 10:01:23 +08:00
|
|
|
)
|
2026-05-23 22:10:20 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-09 10:01:23 +08:00
|
|
|
|
2026-05-27 22:06:32 +08:00
|
|
|
func maxBodySizeMiddleware() gin.HandlerFunc {
|
|
|
|
|
limit := config.Current().Storage.MaxRequestBodySizeBytes()
|
2026-05-23 22:10:20 +08:00
|
|
|
return func(ctx *gin.Context) {
|
|
|
|
|
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, limit)
|
|
|
|
|
ctx.Next()
|
|
|
|
|
}
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 22:10:20 +08:00
|
|
|
func isWebsocketUpgrade(ctx *gin.Context) bool {
|
2026-04-09 10:01:23 +08:00
|
|
|
if !strings.EqualFold(ctx.GetHeader("Upgrade"), "websocket") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return strings.Contains(strings.ToLower(ctx.GetHeader("Connection")), "upgrade")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 22:10:20 +08:00
|
|
|
func addRouter(app *gin.Engine) {
|
|
|
|
|
app.Any("/api/mcp", gin.WrapH(mcps.NewHTTPHandler()))
|
|
|
|
|
|
|
|
|
|
apiGroup := app.Group("/api")
|
2026-05-23 22:12:13 +08:00
|
|
|
registerApiAuthRoutes(apiGroup.Group("/auth"))
|
|
|
|
|
registerApiChannelRoutes(apiGroup.Group("/channel"))
|
|
|
|
|
registerApiCustomerRoutes(apiGroup.Group("/customer"))
|
|
|
|
|
registerApiConversationRoutes(apiGroup.Group("/conversation", middleware.ExternalUserMiddleware))
|
|
|
|
|
registerApiMessageRoutes(apiGroup.Group("/message", middleware.ExternalUserMiddleware))
|
2026-05-23 22:10:20 +08:00
|
|
|
|
|
|
|
|
wsGroup := app.Group("/api/ws")
|
|
|
|
|
wsGroup.GET("/dashboard", middleware.AuthMiddleware, services.WsService.HandleDashboardWS)
|
|
|
|
|
wsGroup.GET("/dashboard/notification", middleware.AuthMiddleware, services.WsService.HandleDashboardNotificationWS)
|
|
|
|
|
wsGroup.GET("/open", services.WsService.HandleOpenWS)
|
|
|
|
|
|
|
|
|
|
dashboardGroup := app.Group("/api/dashboard", middleware.AuthMiddleware)
|
2026-05-23 22:12:13 +08:00
|
|
|
registerDashboardDashboardRoutes(dashboardGroup.Group("/dashboard"))
|
|
|
|
|
registerDashboardUserRoutes(dashboardGroup.Group("/user"))
|
|
|
|
|
registerDashboardCompanyRoutes(dashboardGroup.Group("/company"))
|
|
|
|
|
registerDashboardCustomerRoutes(dashboardGroup.Group("/customer"))
|
|
|
|
|
registerDashboardCustomerContactRoutes(dashboardGroup.Group("/customer-contact"))
|
|
|
|
|
registerDashboardRoleRoutes(dashboardGroup.Group("/role"))
|
|
|
|
|
registerDashboardPermissionRoutes(dashboardGroup.Group("/permission"))
|
|
|
|
|
registerDashboardSessionRoutes(dashboardGroup.Group("/session"))
|
|
|
|
|
registerDashboardTagRoutes(dashboardGroup.Group("/tag"))
|
|
|
|
|
registerDashboardConversationRoutes(dashboardGroup.Group("/conversation"))
|
|
|
|
|
registerDashboardTicketRoutes(dashboardGroup.Group("/ticket"))
|
|
|
|
|
registerDashboardNotificationRoutes(dashboardGroup.Group("/notification"))
|
|
|
|
|
registerDashboardQuickReplyRoutes(dashboardGroup.Group("/quick-reply"))
|
|
|
|
|
registerDashboardChannelRoutes(dashboardGroup.Group("/channel"))
|
|
|
|
|
registerDashboardAgentRoutes(dashboardGroup.Group("/agent"))
|
|
|
|
|
registerDashboardAgentTeamRoutes(dashboardGroup.Group("/agent-team"))
|
|
|
|
|
registerDashboardAgentTeamScheduleRoutes(dashboardGroup.Group("/agent-team-schedule"))
|
|
|
|
|
registerDashboardAIAgentRoutes(dashboardGroup.Group("/ai-agent"))
|
|
|
|
|
registerDashboardAIConfigRoutes(dashboardGroup.Group("/ai-config"))
|
|
|
|
|
registerDashboardAssetRoutes(dashboardGroup.Group("/asset"))
|
|
|
|
|
registerDashboardKnowledgeBaseRoutes(dashboardGroup.Group("/knowledge-base"))
|
2026-06-02 15:37:50 +08:00
|
|
|
registerDashboardKnowledgeDirectoryRoutes(dashboardGroup.Group("/knowledge-directory"))
|
2026-05-23 22:12:13 +08:00
|
|
|
registerDashboardKnowledgeDocumentRoutes(dashboardGroup.Group("/knowledge-document"))
|
|
|
|
|
registerDashboardKnowledgeFAQRoutes(dashboardGroup.Group("/knowledge-faq"))
|
|
|
|
|
registerDashboardKnowledgeRetrieveRoutes(dashboardGroup.Group("/knowledge-retrieve"))
|
|
|
|
|
registerDashboardKnowledgeRetrieveLogRoutes(dashboardGroup.Group("/knowledge-retrieve-log"))
|
|
|
|
|
registerDashboardAgentRunLogRoutes(dashboardGroup.Group("/agent-run-log"))
|
|
|
|
|
registerDashboardSkillDefinitionRoutes(dashboardGroup.Group("/skill-definition"))
|
|
|
|
|
registerDashboardMCPRoutes(dashboardGroup.Group("/mcp"))
|
2026-05-23 22:10:20 +08:00
|
|
|
|
|
|
|
|
thirdGroup := app.Group("/api/third")
|
2026-05-23 22:12:13 +08:00
|
|
|
registerThirdWechatRoutes(thirdGroup.Group("/wechat"))
|
2026-05-23 22:10:20 +08:00
|
|
|
}
|