diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2d4c349..d98b508 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,10 @@ "Bash(rm:*)", "Bash(gofmt:*)", "Bash(goimports:*)", - "Bash(chmod:*)" + "Bash(chmod:*)", + "Bash(true)", + "Bash(go vet:*)", + "Bash(golangci-lint run:*)" ], "deny": [] } diff --git a/app_server.go b/app_server.go new file mode 100644 index 0000000..83d2e72 --- /dev/null +++ b/app_server.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// AppServer 应用服务器结构体,封装所有服务和处理器 +type AppServer struct { + xiaohongshuService *XiaohongshuService + router *gin.Engine + httpServer *http.Server +} + +// NewAppServer 创建新的应用服务器实例 +func NewAppServer(xiaohongshuService *XiaohongshuService) *AppServer { + return &AppServer{ + xiaohongshuService: xiaohongshuService, + } +} + +// Start 启动服务器 +func (s *AppServer) Start(port string) error { + s.router = setupRoutes(s) + + s.httpServer = &http.Server{ + Addr: port, + Handler: s.router, + } + + // 启动服务器的 goroutine + go func() { + logrus.Infof("启动 HTTP 服务器: %s", port) + if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logrus.Errorf("服务器启动失败: %v", err) + os.Exit(1) + } + }() + + // 等待中断信号 + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + logrus.Infof("正在关闭服务器...") + + // 优雅关闭 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // 关闭 HTTP 服务器 + if err := s.httpServer.Shutdown(ctx); err != nil { + logrus.Errorf("服务器关闭失败: %v", err) + return err + } + + logrus.Infof("服务器已关闭") + return nil +} diff --git a/handlers.go b/handlers_api.go similarity index 54% rename from handlers.go rename to handlers_api.go index 947865a..5431c1f 100644 --- a/handlers.go +++ b/handlers_api.go @@ -7,25 +7,6 @@ import ( "github.com/sirupsen/logrus" ) -// LoginWaitRequest 等待登录请求 -type LoginWaitRequest struct { - Timeout int `json:"timeout"` -} - -// ErrorResponse 错误响应 -type ErrorResponse struct { - Error string `json:"error"` - Code string `json:"code"` - Details any `json:"details,omitempty"` -} - -// SuccessResponse 成功响应 -type SuccessResponse struct { - Success bool `json:"success"` - Data any `json:"data"` - Message string `json:"message,omitempty"` -} - // respondError 返回错误响应 func respondError(c *gin.Context, statusCode int, code, message string, details any) { response := ErrorResponse{ @@ -54,12 +35,9 @@ func respondSuccess(c *gin.Context, data any, message string) { c.JSON(http.StatusOK, response) } -// XiaohongshuService 全局服务实例 -var xiaohongshuService = NewXiaohongshuService() - // checkLoginStatusHandler 检查登录状态 -func checkLoginStatusHandler(c *gin.Context) { - status, err := xiaohongshuService.CheckLoginStatus(c.Request.Context()) +func (s *AppServer) checkLoginStatusHandler(c *gin.Context) { + status, err := s.xiaohongshuService.CheckLoginStatus(c.Request.Context()) if err != nil { respondError(c, http.StatusInternalServerError, "STATUS_CHECK_FAILED", "检查登录状态失败", err.Error()) @@ -71,7 +49,7 @@ func checkLoginStatusHandler(c *gin.Context) { } // publishHandler 发布内容 -func publishHandler(c *gin.Context) { +func (s *AppServer) publishHandler(c *gin.Context) { var req PublishRequest if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, "INVALID_REQUEST", @@ -80,7 +58,7 @@ func publishHandler(c *gin.Context) { } // 执行发布 - result, err := xiaohongshuService.PublishContent(c.Request.Context(), &req) + result, err := s.xiaohongshuService.PublishContent(c.Request.Context(), &req) if err != nil { respondError(c, http.StatusInternalServerError, "PUBLISH_FAILED", "发布失败", err.Error()) @@ -91,9 +69,9 @@ func publishHandler(c *gin.Context) { } // listFeedsHandler 获取Feeds列表 -func listFeedsHandler(c *gin.Context) { +func (s *AppServer) listFeedsHandler(c *gin.Context) { // 获取 Feeds 列表 - result, err := xiaohongshuService.ListFeeds(c.Request.Context()) + result, err := s.xiaohongshuService.ListFeeds(c.Request.Context()) if err != nil { respondError(c, http.StatusInternalServerError, "LIST_FEEDS_FAILED", "获取Feeds列表失败", err.Error()) @@ -113,29 +91,3 @@ func healthHandler(c *gin.Context) { "timestamp": "now", }, "服务正常") } - -// corsMiddleware CORS 中间件 -func corsMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("Access-Control-Allow-Origin", "*") - c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(http.StatusNoContent) - return - } - - c.Next() - } -} - -// errorHandlingMiddleware 错误处理中间件 -func errorHandlingMiddleware() gin.HandlerFunc { - return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { - logrus.Errorf("服务器内部错误: %v, path: %s", recovered, c.Request.URL.Path) - - respondError(c, http.StatusInternalServerError, "INTERNAL_ERROR", - "服务器内部错误", recovered) - }) -} diff --git a/mcp_server.go b/handlers_mcp.go similarity index 70% rename from mcp_server.go rename to handlers_mcp.go index 43ac34f..a9cd8f3 100644 --- a/mcp_server.go +++ b/handlers_mcp.go @@ -9,50 +9,13 @@ import ( "github.com/sirupsen/logrus" ) -// JSON-RPC 结构定义 - -type JSONRPCRequest struct { - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` - ID interface{} `json:"id"` -} - -type JSONRPCResponse struct { - JSONRPC string `json:"jsonrpc"` - Result interface{} `json:"result,omitempty"` - Error *JSONRPCError `json:"error,omitempty"` - ID interface{} `json:"id"` -} - -type JSONRPCError struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` -} - -type MCPToolCall struct { - Name string `json:"name"` - Arguments map[string]interface{} `json:"arguments"` -} - -type MCPToolResult struct { - Content []MCPContent `json:"content"` - IsError bool `json:"isError,omitempty"` -} - -type MCPContent struct { - Type string `json:"type"` - Text string `json:"text"` -} - // MCP 工具处理函数 // handleCheckLoginStatus 处理检查登录状态 -func handleCheckLoginStatus(ctx context.Context) *MCPToolResult { +func (s *AppServer) handleCheckLoginStatus(ctx context.Context) *MCPToolResult { logrus.Info("MCP: 检查登录状态") - status, err := xiaohongshuService.CheckLoginStatus(ctx) + status, err := s.xiaohongshuService.CheckLoginStatus(ctx) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ @@ -73,7 +36,7 @@ func handleCheckLoginStatus(ctx context.Context) *MCPToolResult { } // handlePublishContent 处理发布内容 -func handlePublishContent(ctx context.Context, args map[string]interface{}) *MCPToolResult { +func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]interface{}) *MCPToolResult { logrus.Info("MCP: 发布内容") // 解析参数 @@ -98,7 +61,7 @@ func handlePublishContent(ctx context.Context, args map[string]interface{}) *MCP } // 执行发布 - result, err := xiaohongshuService.PublishContent(ctx, req) + result, err := s.xiaohongshuService.PublishContent(ctx, req) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ @@ -119,10 +82,10 @@ func handlePublishContent(ctx context.Context, args map[string]interface{}) *MCP } // handleListFeeds 处理获取Feeds列表 -func handleListFeeds(ctx context.Context) *MCPToolResult { +func (s *AppServer) handleListFeeds(ctx context.Context) *MCPToolResult { logrus.Info("MCP: 获取Feeds列表") - result, err := xiaohongshuService.ListFeeds(ctx) + result, err := s.xiaohongshuService.ListFeeds(ctx) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ @@ -154,11 +117,11 @@ func handleListFeeds(ctx context.Context) *MCPToolResult { } // handleMCPRequest 处理 MCP 请求 -func handleMCPRequest(w http.ResponseWriter, r *http.Request) { +func (s *AppServer) handleMCPRequest(w http.ResponseWriter, r *http.Request) { var req JSONRPCRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { logrus.Errorf("解析请求失败: %v", err) - sendJSONRPCError(w, req.ID, -32700, "Parse error", nil) + s.sendJSONRPCError(w, req.ID, -32700, "Parse error", nil) return } @@ -166,19 +129,19 @@ func handleMCPRequest(w http.ResponseWriter, r *http.Request) { switch req.Method { case "initialize": - handleInitialize(w, req) + s.handleInitialize(w, req) case "tools/list": - handleToolsList(w, req) + s.handleToolsList(w, req) case "tools/call": - handleToolsCall(w, r, req) + s.handleToolsCall(w, r, req) default: logrus.Warnf("不支持的方法: %s", req.Method) - sendJSONRPCError(w, req.ID, -32601, "Method not found", nil) + s.sendJSONRPCError(w, req.ID, -32601, "Method not found", nil) } } // handleInitialize 处理初始化请求 -func handleInitialize(w http.ResponseWriter, req JSONRPCRequest) { +func (s *AppServer) handleInitialize(w http.ResponseWriter, req JSONRPCRequest) { result := map[string]interface{}{ "protocolVersion": "2024-11-05", "capabilities": map[string]interface{}{ @@ -190,11 +153,11 @@ func handleInitialize(w http.ResponseWriter, req JSONRPCRequest) { }, } - sendJSONRPCResponse(w, req.ID, result) + s.sendJSONRPCResponse(w, req.ID, result) } // handleToolsList 处理工具列表请求 -func handleToolsList(w http.ResponseWriter, req JSONRPCRequest) { +func (s *AppServer) handleToolsList(w http.ResponseWriter, req JSONRPCRequest) { tools := []map[string]interface{}{ { "name": "check_login_status", @@ -242,16 +205,16 @@ func handleToolsList(w http.ResponseWriter, req JSONRPCRequest) { "tools": tools, } - sendJSONRPCResponse(w, req.ID, result) + s.sendJSONRPCResponse(w, req.ID, result) } // handleToolsCall 处理工具调用请求 -func handleToolsCall(w http.ResponseWriter, r *http.Request, req JSONRPCRequest) { +func (s *AppServer) handleToolsCall(w http.ResponseWriter, r *http.Request, req JSONRPCRequest) { var toolCall MCPToolCall paramsBytes, _ := json.Marshal(req.Params) if err := json.Unmarshal(paramsBytes, &toolCall); err != nil { logrus.Errorf("解析工具调用参数失败: %v", err) - sendJSONRPCError(w, req.ID, -32602, "Invalid params", nil) + s.sendJSONRPCError(w, req.ID, -32602, "Invalid params", nil) return } @@ -260,22 +223,22 @@ func handleToolsCall(w http.ResponseWriter, r *http.Request, req JSONRPCRequest) switch toolCall.Name { case "check_login_status": - result = handleCheckLoginStatus(ctx) + result = s.handleCheckLoginStatus(ctx) case "publish_content": - result = handlePublishContent(ctx, toolCall.Arguments) + result = s.handlePublishContent(ctx, toolCall.Arguments) case "list_feeds": - result = handleListFeeds(ctx) + result = s.handleListFeeds(ctx) default: logrus.Warnf("不支持的工具: %s", toolCall.Name) - sendJSONRPCError(w, req.ID, -32601, "Tool not found", nil) + s.sendJSONRPCError(w, req.ID, -32601, "Tool not found", nil) return } - sendJSONRPCResponse(w, req.ID, result) + s.sendJSONRPCResponse(w, req.ID, result) } // sendJSONRPCResponse 发送JSON-RPC响应 -func sendJSONRPCResponse(w http.ResponseWriter, id interface{}, result interface{}) { +func (s *AppServer) sendJSONRPCResponse(w http.ResponseWriter, id interface{}, result interface{}) { response := JSONRPCResponse{ JSONRPC: "2.0", Result: result, @@ -283,11 +246,13 @@ func sendJSONRPCResponse(w http.ResponseWriter, id interface{}, result interface } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + logrus.Errorf("Failed to encode JSON-RPC response: %v", err) + } } // sendJSONRPCError 发送JSON-RPC错误响应 -func sendJSONRPCError(w http.ResponseWriter, id interface{}, code int, message string, data interface{}) { +func (s *AppServer) sendJSONRPCError(w http.ResponseWriter, id interface{}, code int, message string, data interface{}) { response := JSONRPCResponse{ JSONRPC: "2.0", Error: &JSONRPCError{ @@ -300,11 +265,13 @@ func sendJSONRPCError(w http.ResponseWriter, id interface{}, code int, message s w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // JSON-RPC错误仍然返回200状态码 - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + logrus.Errorf("Failed to encode JSON-RPC error response: %v", err) + } } // createMCPHandler 创建MCP HTTP处理器 -func createMCPHandler() http.HandlerFunc { +func (s *AppServer) createMCPHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // 设置 CORS 头 w.Header().Set("Access-Control-Allow-Origin", "*") @@ -323,6 +290,6 @@ func createMCPHandler() http.HandlerFunc { } // 处理 MCP JSON-RPC 请求 - handleMCPRequest(w, r) + s.handleMCPRequest(w, r) } } diff --git a/main.go b/main.go index 2b2d6a0..914c636 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,6 @@ import ( ) func main() { - var ( headless bool ) @@ -17,7 +16,12 @@ func main() { configs.InitHeadless(headless) - if err := startServer(); err != nil { + // 初始化服务 + xiaohongshuService := NewXiaohongshuService() + + // 创建并启动应用服务器 + appServer := NewAppServer(xiaohongshuService) + if err := appServer.Start(":18060"); err != nil { logrus.Fatalf("failed to run server: %v", err) } } diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..b38164a --- /dev/null +++ b/middleware.go @@ -0,0 +1,34 @@ +package main + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// corsMiddleware CORS 中间件 +func corsMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + return + } + + c.Next() + } +} + +// errorHandlingMiddleware 错误处理中间件 +func errorHandlingMiddleware() gin.HandlerFunc { + return gin.CustomRecovery(func(c *gin.Context, recovered any) { + logrus.Errorf("服务器内部错误: %v, path: %s", recovered, c.Request.URL.Path) + + respondError(c, http.StatusInternalServerError, "INTERNAL_ERROR", + "服务器内部错误", recovered) + }) +} diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..0401d0a --- /dev/null +++ b/routes.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/gin-gonic/gin" +) + +// setupRoutes 设置路由配置 +func setupRoutes(appServer *AppServer) *gin.Engine { + // 设置 Gin 模式 + gin.SetMode(gin.ReleaseMode) + + router := gin.New() + router.Use(gin.Logger()) + router.Use(gin.Recovery()) + + // 添加中间件 + router.Use(errorHandlingMiddleware()) + router.Use(corsMiddleware()) + + // 健康检查 + router.GET("/health", healthHandler) + + // MCP 端点 - 使用 SSE 协议 + mcpHandler := appServer.createMCPHandler() + router.Any("/mcp", gin.WrapH(mcpHandler)) + router.Any("/mcp/*path", gin.WrapH(mcpHandler)) + + // API 路由组 + api := router.Group("/api/v1") + { + api.GET("/login/status", appServer.checkLoginStatusHandler) + api.POST("/publish", appServer.publishHandler) + api.GET("/feeds/list", appServer.listFeedsHandler) + } + + return router +} diff --git a/server.go b/server.go deleted file mode 100644 index f1e98e2..0000000 --- a/server.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "context" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// setupRouter 设置路由 -func setupRouter() *gin.Engine { - // 设置 Gin 模式 - gin.SetMode(gin.ReleaseMode) - - router := gin.New() - - router.Use(gin.Logger()) - router.Use(gin.Recovery()) - - // 添加中间件 - router.Use(errorHandlingMiddleware()) - router.Use(corsMiddleware()) - - // 健康检查 - router.GET("/health", healthHandler) - - // MCP 端点 - 使用 SSE 协议 - mcpHandler := createMCPHandler() - router.Any("/mcp", gin.WrapH(mcpHandler)) - router.Any("/mcp/*path", gin.WrapH(mcpHandler)) - - // API 路由组 - api := router.Group("/api/v1") - { - api.GET("/login/status", checkLoginStatusHandler) - - api.POST("/publish", publishHandler) - - // Feeds 相关路由 - api.GET("/feeds/list", listFeedsHandler) - } - - return router -} - -// startServer 启动服务器 -func startServer() error { - router := setupRouter() - - port := ":18060" - server := &http.Server{ - Addr: port, - Handler: router, - } - - // 启动服务器的 goroutine - go func() { - logrus.Infof("启动 HTTP 服务器: %s", port) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logrus.Errorf("服务器启动失败: %v", err) - os.Exit(1) - } - }() - - // 等待中断信号 - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - logrus.Infof("正在关闭服务器...") - - // 优雅关闭 - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - // 关闭 HTTP 服务器 - if err := server.Shutdown(ctx); err != nil { - logrus.Errorf("服务器关闭失败: %v", err) - return err - } - - logrus.Infof("服务器已关闭") - return nil -} diff --git a/types.go b/types.go new file mode 100644 index 0000000..6f3b3ec --- /dev/null +++ b/types.go @@ -0,0 +1,62 @@ +package main + +// HTTP API 响应类型 + +// ErrorResponse 错误响应 +type ErrorResponse struct { + Error string `json:"error"` + Code string `json:"code"` + Details any `json:"details,omitempty"` +} + +// SuccessResponse 成功响应 +type SuccessResponse struct { + Success bool `json:"success"` + Data any `json:"data"` + Message string `json:"message,omitempty"` +} + +// JSON-RPC 相关类型 + +// JSONRPCRequest JSON-RPC 请求 +type JSONRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params any `json:"params,omitempty"` + ID any `json:"id"` +} + +// JSONRPCResponse JSON-RPC 响应 +type JSONRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + Result any `json:"result,omitempty"` + Error *JSONRPCError `json:"error,omitempty"` + ID any `json:"id"` +} + +// JSONRPCError JSON-RPC 错误 +type JSONRPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data any `json:"data,omitempty"` +} + +// MCP 相关类型 + +// MCPToolCall MCP 工具调用 +type MCPToolCall struct { + Name string `json:"name"` + Arguments map[string]interface{} `json:"arguments"` +} + +// MCPToolResult MCP 工具结果 +type MCPToolResult struct { + Content []MCPContent `json:"content"` + IsError bool `json:"isError,omitempty"` +} + +// MCPContent MCP 内容 +type MCPContent struct { + Type string `json:"type"` + Text string `json:"text"` +}