From c6680558e30ad11faa73f831a71c062c73aba6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Mon, 21 Apr 2025 10:55:14 +0800 Subject: [PATCH 01/19] =?UTF-8?q?refactor(server):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=90=AF=E5=8A=A8=E5=92=8C=E9=87=8D?= =?UTF-8?q?=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将服务器启动和重载逻辑进行重构,提取初始化系统为单独函数,优化代码结构。删除冗余的服务器初始化文件,统一使用新的 `server_run.go` 实现优雅关闭和重载功能。同时,将“重启服务”改为“重载服务”以更准确地描述功能。 --- server/api/v1/system/sys_system.go | 10 ++-- server/core/server.go | 4 -- server/core/server_other.go | 19 ------ server/core/server_run.go | 70 ++++++++++++++++++++++ server/core/server_win.go | 21 ------- server/main.go | 14 +++-- server/utils/reload.go | 48 +++++++++++---- web/src/api/system.js | 2 +- web/src/pathInfo.json | 2 +- web/src/view/systemTools/system/system.vue | 4 +- 10 files changed, 127 insertions(+), 67 deletions(-) delete mode 100644 server/core/server_other.go create mode 100644 server/core/server_run.go delete mode 100644 server/core/server_win.go diff --git a/server/api/v1/system/sys_system.go b/server/api/v1/system/sys_system.go index aa41c2f469..931d443f53 100644 --- a/server/api/v1/system/sys_system.go +++ b/server/api/v1/system/sys_system.go @@ -55,19 +55,19 @@ func (s *SystemApi) SetSystemConfig(c *gin.Context) { // ReloadSystem // @Tags System -// @Summary 重启系统 +// @Summary 重载系统 // @Security ApiKeyAuth // @Produce application/json -// @Success 200 {object} response.Response{msg=string} "重启系统" +// @Success 200 {object} response.Response{msg=string} "重载系统" // @Router /system/reloadSystem [post] func (s *SystemApi) ReloadSystem(c *gin.Context) { err := utils.Reload() if err != nil { - global.GVA_LOG.Error("重启系统失败!", zap.Error(err)) - response.FailWithMessage("重启系统失败", c) + global.GVA_LOG.Error("重载系统失败!", zap.Error(err)) + response.FailWithMessage("重载系统失败:"+err.Error(), c) return } - response.OkWithMessage("重启系统成功", c) + response.OkWithMessage("重载系统成功", c) } // GetServerInfo diff --git a/server/core/server.go b/server/core/server.go index a58224b2df..eccff7064f 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -8,10 +8,6 @@ import ( "go.uber.org/zap" ) -type server interface { - ListenAndServe() error -} - func RunWindowsServer() { if global.GVA_CONFIG.System.UseMultipoint || global.GVA_CONFIG.System.UseRedis { // 初始化redis服务 diff --git a/server/core/server_other.go b/server/core/server_other.go deleted file mode 100644 index 83645fced0..0000000000 --- a/server/core/server_other.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !windows -// +build !windows - -package core - -import ( - "time" - - "github.com/fvbock/endless" - "github.com/gin-gonic/gin" -) - -func initServer(address string, router *gin.Engine) server { - s := endless.NewServer(address, router) - s.ReadHeaderTimeout = 10 * time.Minute - s.WriteTimeout = 10 * time.Minute - s.MaxHeaderBytes = 1 << 20 - return s -} diff --git a/server/core/server_run.go b/server/core/server_run.go new file mode 100644 index 0000000000..bd41e9c9ca --- /dev/null +++ b/server/core/server_run.go @@ -0,0 +1,70 @@ +package core + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type server interface { + ListenAndServe() error + Shutdown(context.Context) error +} + +// 初始化服务 +func initServer(address string, router *gin.Engine) server { + return &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: 10 * time.Minute, + WriteTimeout: 10 * time.Minute, + MaxHeaderBytes: 1 << 20, + } +} + +// RunServer 启动服务并实现优雅关闭 +func RunServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { + // 创建服务 + srv := &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: 1 << 20, + } + + // 在goroutine中启动服务 + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("listen: %s\n", err) + zap.L().Error("server启动失败", zap.Error(err)) + os.Exit(1) + } + }() + + // 等待中断信号以优雅地关闭服务器 + quit := make(chan os.Signal, 1) + // kill (无参数) 默认发送 syscall.SIGTERM + // kill -2 发送 syscall.SIGINT + // kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加 + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + zap.L().Info("关闭服务器...") + + // 设置5秒的超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + zap.L().Fatal("服务器关闭异常", zap.Error(err)) + } + + zap.L().Info("服务器已优雅关闭") +} diff --git a/server/core/server_win.go b/server/core/server_win.go deleted file mode 100644 index 20cf44b9f2..0000000000 --- a/server/core/server_win.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build windows -// +build windows - -package core - -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" -) - -func initServer(address string, router *gin.Engine) server { - return &http.Server{ - Addr: address, - Handler: router, - ReadTimeout: 10 * time.Minute, - WriteTimeout: 10 * time.Minute, - MaxHeaderBytes: 1 << 20, - } -} diff --git a/server/main.go b/server/main.go index deaac4bad4..8e27a39894 100644 --- a/server/main.go +++ b/server/main.go @@ -28,6 +28,16 @@ import ( // @name x-token // @BasePath / func main() { + // 初始化系统 + initializeSystem() + + // 运行服务器 + core.RunWindowsServer() +} + +// initializeSystem 初始化系统所有组件 +// 提取为单独函数以便于系统重载时调用 +func initializeSystem() { global.GVA_VP = core.Viper() // 初始化Viper initialize.OtherInit() global.GVA_LOG = core.Zap() // 初始化zap日志库 @@ -37,9 +47,5 @@ func main() { initialize.DBList() if global.GVA_DB != nil { initialize.RegisterTables() // 初始化表 - // 程序结束前关闭数据库链接 - db, _ := global.GVA_DB.DB() - defer db.Close() } - core.RunWindowsServer() } diff --git a/server/utils/reload.go b/server/utils/reload.go index de5499bf33..ff2abbeb82 100644 --- a/server/utils/reload.go +++ b/server/utils/reload.go @@ -1,18 +1,46 @@ package utils import ( - "errors" - "os" - "os/exec" - "runtime" - "strconv" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize" + "go.uber.org/zap" ) +// Reload 优雅地重新加载系统配置 func Reload() error { - if runtime.GOOS == "windows" { - return errors.New("系统不支持") + global.GVA_LOG.Info("正在重新加载系统配置...") + + // 重新加载配置文件 + if err := global.GVA_VP.ReadInConfig(); err != nil { + global.GVA_LOG.Error("重新读取配置文件失败!", zap.Error(err)) + return err + } + + // 重新初始化数据库连接 + if global.GVA_DB != nil { + db, _ := global.GVA_DB.DB() + err := db.Close() + if err != nil { + global.GVA_LOG.Error("关闭原数据库连接失败!", zap.Error(err)) + return err + } } - pid := os.Getpid() - cmd := exec.Command("kill", "-1", strconv.Itoa(pid)) - return cmd.Run() + + // 重新建立数据库连接 + global.GVA_DB = initialize.Gorm() + + // 重新初始化其他配置 + initialize.OtherInit() + initialize.DBList() + + if global.GVA_DB != nil { + // 确保数据库表结构是最新的 + initialize.RegisterTables() + } + + // 重新初始化定时任务 + initialize.Timer() + + global.GVA_LOG.Info("系统配置重新加载完成") + return nil } diff --git a/web/src/api/system.js b/web/src/api/system.js index 9395519a3a..ff41abfad6 100644 --- a/web/src/api/system.js +++ b/web/src/api/system.js @@ -42,7 +42,7 @@ export const getSystemState = () => { } /** - * 重启服务 + * 重载服务 * @param data * @returns {*} */ diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index 05c02a6cde..369a6e069a 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -26,6 +26,7 @@ "/src/view/layout/aside/headMode.vue": "GvaAside", "/src/view/layout/aside/index.vue": "Index", "/src/view/layout/aside/normalMode.vue": "GvaAside", + "/src/view/layout/aside/sidebarMode.vue": "SidebarMode", "/src/view/layout/header/index.vue": "Index", "/src/view/layout/header/tools.vue": "Tools", "/src/view/layout/iframe.vue": "GvaLayoutIframe", @@ -54,7 +55,6 @@ "/src/view/superAdmin/user/user.vue": "User", "/src/view/system/state.vue": "State", "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog", - "/src/view/systemTools/autoCode/component/iframeRenderer.vue": "IframeRenderer", "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog", "/src/view/systemTools/autoCode/index.vue": "AutoCode", "/src/view/systemTools/autoCode/picture.vue": "Picture", diff --git a/web/src/view/systemTools/system/system.vue b/web/src/view/systemTools/system/system.vue index dfbc913255..bd098a1f1f 100644 --- a/web/src/view/systemTools/system/system.vue +++ b/web/src/view/systemTools/system/system.vue @@ -938,7 +938,7 @@
立即更新 - 重启服务 + 重载服务
@@ -1010,7 +1010,7 @@ } initForm() const reload = () => { - ElMessageBox.confirm('确定要重启服务?', '警告', { + ElMessageBox.confirm('确定要重载服务?', '警告', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' From 70f68d0462a2ced93692fa1621dc46b826728a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Mon, 21 Apr 2025 11:38:28 +0800 Subject: [PATCH 02/19] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86=E3=80=81JWT?= =?UTF-8?q?=E5=92=8CCasbin=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将系统重载逻辑提取到独立的`system_events.go`文件中,并引入全局事件管理器 - 将JWT相关操作从`service`层移动到`utils`层,减少服务层依赖 - 将Casbin实例管理逻辑提取到`utils`层,统一管理Casbin实例的初始化和获取 - 删除冗余的`CreateSysOperationRecord`方法,优化操作记录中间件逻辑 --- server/api/v1/system/sys_operation_record.go | 25 -------- server/api/v1/system/sys_system.go | 3 +- server/api/v1/system/sys_user.go | 4 +- server/initialize/init.go | 15 +++++ server/{utils => initialize}/reload.go | 13 ++-- server/middleware/casbin_rbac.go | 5 +- server/middleware/email.go | 8 +-- server/middleware/jwt.go | 18 ++++-- server/middleware/operation.go | 6 +- server/router/system/sys_operation_record.go | 1 - server/service/system/jwt_black_list.go | 32 ---------- server/service/system/sys_casbin.go | 60 ++----------------- server/service/system/sys_operation_record.go | 5 -- server/utils/casbin_util.go | 52 ++++++++++++++++ server/utils/jwt.go | 18 ++++++ server/utils/system_events.go | 34 +++++++++++ 16 files changed, 153 insertions(+), 146 deletions(-) create mode 100644 server/initialize/init.go rename server/{utils => initialize}/reload.go (80%) create mode 100644 server/utils/casbin_util.go create mode 100644 server/utils/system_events.go diff --git a/server/api/v1/system/sys_operation_record.go b/server/api/v1/system/sys_operation_record.go index 40daeb98db..c88511f765 100644 --- a/server/api/v1/system/sys_operation_record.go +++ b/server/api/v1/system/sys_operation_record.go @@ -13,31 +13,6 @@ import ( type OperationRecordApi struct{} -// CreateSysOperationRecord -// @Tags SysOperationRecord -// @Summary 创建SysOperationRecord -// @Security ApiKeyAuth -// @accept application/json -// @Produce application/json -// @Param data body system.SysOperationRecord true "创建SysOperationRecord" -// @Success 200 {object} response.Response{msg=string} "创建SysOperationRecord" -// @Router /sysOperationRecord/createSysOperationRecord [post] -func (s *OperationRecordApi) CreateSysOperationRecord(c *gin.Context) { - var sysOperationRecord system.SysOperationRecord - err := c.ShouldBindJSON(&sysOperationRecord) - if err != nil { - response.FailWithMessage(err.Error(), c) - return - } - err = operationRecordService.CreateSysOperationRecord(sysOperationRecord) - if err != nil { - global.GVA_LOG.Error("创建失败!", zap.Error(err)) - response.FailWithMessage("创建失败", c) - return - } - response.OkWithMessage("创建成功", c) -} - // DeleteSysOperationRecord // @Tags SysOperationRecord // @Summary 删除SysOperationRecord diff --git a/server/api/v1/system/sys_system.go b/server/api/v1/system/sys_system.go index 931d443f53..6dabb0abed 100644 --- a/server/api/v1/system/sys_system.go +++ b/server/api/v1/system/sys_system.go @@ -61,7 +61,8 @@ func (s *SystemApi) SetSystemConfig(c *gin.Context) { // @Success 200 {object} response.Response{msg=string} "重载系统" // @Router /system/reloadSystem [post] func (s *SystemApi) ReloadSystem(c *gin.Context) { - err := utils.Reload() + // 触发系统重载事件 + err := utils.GlobalSystemEvents.TriggerReload() if err != nil { global.GVA_LOG.Error("重载系统失败!", zap.Error(err)) response.FailWithMessage("重载系统失败:"+err.Error(), c) diff --git a/server/api/v1/system/sys_user.go b/server/api/v1/system/sys_user.go index e531bf3fd1..ae763f8bb5 100644 --- a/server/api/v1/system/sys_user.go +++ b/server/api/v1/system/sys_user.go @@ -93,7 +93,7 @@ func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { } if jwtStr, err := jwtService.GetRedisJWT(user.Username); err == redis.Nil { - if err := jwtService.SetRedisJWT(token, user.Username); err != nil { + if err := utils.SetRedisJWT(token, user.Username); err != nil { global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err)) response.FailWithMessage("设置登录状态失败", c) return @@ -114,7 +114,7 @@ func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { response.FailWithMessage("jwt作废失败", c) return } - if err := jwtService.SetRedisJWT(token, user.GetUsername()); err != nil { + if err := utils.SetRedisJWT(token, user.GetUsername()); err != nil { response.FailWithMessage("设置登录状态失败", c) return } diff --git a/server/initialize/init.go b/server/initialize/init.go new file mode 100644 index 0000000000..f4955cbda2 --- /dev/null +++ b/server/initialize/init.go @@ -0,0 +1,15 @@ +// 假设这是初始化逻辑的一部分 + +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +// 初始化函数,在应用启动时调用 +func SetupReloadHandlers() { + // 注册系统重载处理函数 + utils.GlobalSystemEvents.RegisterReloadHandler(func() error { + return Reload() + }) +} diff --git a/server/utils/reload.go b/server/initialize/reload.go similarity index 80% rename from server/utils/reload.go rename to server/initialize/reload.go index ff2abbeb82..8fd27e6913 100644 --- a/server/utils/reload.go +++ b/server/initialize/reload.go @@ -1,8 +1,7 @@ -package utils +package initialize import ( "github.com/flipped-aurora/gin-vue-admin/server/global" - "github.com/flipped-aurora/gin-vue-admin/server/initialize" "go.uber.org/zap" ) @@ -27,19 +26,19 @@ func Reload() error { } // 重新建立数据库连接 - global.GVA_DB = initialize.Gorm() + global.GVA_DB = Gorm() // 重新初始化其他配置 - initialize.OtherInit() - initialize.DBList() + OtherInit() + DBList() if global.GVA_DB != nil { // 确保数据库表结构是最新的 - initialize.RegisterTables() + RegisterTables() } // 重新初始化定时任务 - initialize.Timer() + Timer() global.GVA_LOG.Info("系统配置重新加载完成") return nil diff --git a/server/middleware/casbin_rbac.go b/server/middleware/casbin_rbac.go index a1ca4c2b7b..6744c758b5 100644 --- a/server/middleware/casbin_rbac.go +++ b/server/middleware/casbin_rbac.go @@ -6,13 +6,10 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" - "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" ) -var casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService - // CasbinHandler 拦截器 func CasbinHandler() gin.HandlerFunc { return func(c *gin.Context) { @@ -24,7 +21,7 @@ func CasbinHandler() gin.HandlerFunc { act := c.Request.Method // 获取用户的角色 sub := strconv.Itoa(int(waitUse.AuthorityId)) - e := casbinService.Casbin() // 判断策略中是否存在 + e := utils.GetCasbin() // 判断策略中是否存在 success, _ := e.Enforce(sub, obj, act) if !success { response.FailWithDetailed(gin.H{}, "权限不足", c) diff --git a/server/middleware/email.go b/server/middleware/email.go index 4a07561c98..1bc976cfbf 100644 --- a/server/middleware/email.go +++ b/server/middleware/email.go @@ -11,13 +11,10 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" - "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) -var userService = service.ServiceGroupApp.SystemServiceGroup.UserService - func ErrorToEmail() gin.HandlerFunc { return func(c *gin.Context) { var username string @@ -26,11 +23,12 @@ func ErrorToEmail() gin.HandlerFunc { username = claims.Username } else { id, _ := strconv.Atoi(c.Request.Header.Get("x-user-id")) - user, err := userService.FindUserById(id) + var u system.SysUser + err := global.GVA_DB.Where("id = ?", id).First(&u).Error if err != nil { username = "Unknown" } - username = user.Username + username = u.Username } body, _ := io.ReadAll(c.Request.Body) // 再重新写回请求体body中,ioutil.ReadAll会清空c.Request.Body中的数据 diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go index 291d5e3d7c..db71ed6b97 100644 --- a/server/middleware/jwt.go +++ b/server/middleware/jwt.go @@ -9,12 +9,9 @@ import ( "time" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" - "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/gin-gonic/gin" ) -var jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService - func JWTAuth() gin.HandlerFunc { return func(c *gin.Context) { // 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 @@ -24,7 +21,7 @@ func JWTAuth() gin.HandlerFunc { c.Abort() return } - if jwtService.IsBlacklist(token) { + if isBlacklist(token) { response.NoAuth("您的帐户异地登陆或令牌失效", c) utils.ClearToken(c) c.Abort() @@ -65,7 +62,7 @@ func JWTAuth() gin.HandlerFunc { utils.SetToken(c, newToken, int(dr.Seconds())) if global.GVA_CONFIG.System.UseMultipoint { // 记录新的活跃jwt - _ = jwtService.SetRedisJWT(newToken, newClaims.Username) + _ = utils.SetRedisJWT(newToken, newClaims.Username) } } c.Next() @@ -78,3 +75,14 @@ func JWTAuth() gin.HandlerFunc { } } } + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: IsBlacklist +//@description: 判断JWT是否在黑名单内部 +//@param: jwt string +//@return: bool + +func isBlacklist(jwt string) bool { + _, ok := global.BlackCache.Get(jwt) + return ok +} diff --git a/server/middleware/operation.go b/server/middleware/operation.go index f34cf68ee2..dd545f56fc 100644 --- a/server/middleware/operation.go +++ b/server/middleware/operation.go @@ -15,13 +15,10 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" - "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) -var operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService - var respPool sync.Pool var bufferSize = 1024 @@ -115,8 +112,7 @@ func OperationRecord() gin.HandlerFunc { record.Body = "超出记录长度" } } - - if err := operationRecordService.CreateSysOperationRecord(record); err != nil { + if err := global.GVA_DB.Create(&record).Error; err != nil { global.GVA_LOG.Error("create operation record error:", zap.Error(err)) } } diff --git a/server/router/system/sys_operation_record.go b/server/router/system/sys_operation_record.go index 11b841db70..d158d5e704 100644 --- a/server/router/system/sys_operation_record.go +++ b/server/router/system/sys_operation_record.go @@ -9,7 +9,6 @@ type OperationRecordRouter struct{} func (s *OperationRecordRouter) InitSysOperationRecordRouter(Router *gin.RouterGroup) { operationRecordRouter := Router.Group("sysOperationRecord") { - operationRecordRouter.POST("createSysOperationRecord", operationRecordApi.CreateSysOperationRecord) // 新建SysOperationRecord operationRecordRouter.DELETE("deleteSysOperationRecord", operationRecordApi.DeleteSysOperationRecord) // 删除SysOperationRecord operationRecordRouter.DELETE("deleteSysOperationRecordByIds", operationRecordApi.DeleteSysOperationRecordByIds) // 批量删除SysOperationRecord operationRecordRouter.GET("findSysOperationRecord", operationRecordApi.FindSysOperationRecord) // 根据ID获取SysOperationRecord diff --git a/server/service/system/jwt_black_list.go b/server/service/system/jwt_black_list.go index 78ae38a7ea..6c34bbbac1 100644 --- a/server/service/system/jwt_black_list.go +++ b/server/service/system/jwt_black_list.go @@ -7,7 +7,6 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" - "github.com/flipped-aurora/gin-vue-admin/server/utils" ) type JwtService struct{} @@ -29,20 +28,6 @@ func (jwtService *JwtService) JsonInBlacklist(jwtList system.JwtBlacklist) (err return } -//@author: [piexlmax](https://github.com/piexlmax) -//@function: IsBlacklist -//@description: 判断JWT是否在黑名单内部 -//@param: jwt string -//@return: bool - -func (jwtService *JwtService) IsBlacklist(jwt string) bool { - _, ok := global.BlackCache.Get(jwt) - return ok - // err := global.GVA_DB.Where("jwt = ?", jwt).First(&system.JwtBlacklist{}).Error - // isNotFound := errors.Is(err, gorm.ErrRecordNotFound) - // return !isNotFound -} - //@author: [piexlmax](https://github.com/piexlmax) //@function: GetRedisJWT //@description: 从redis取jwt @@ -54,23 +39,6 @@ func (jwtService *JwtService) GetRedisJWT(userName string) (redisJWT string, err return redisJWT, err } -//@author: [piexlmax](https://github.com/piexlmax) -//@function: SetRedisJWT -//@description: jwt存入redis并设置过期时间 -//@param: jwt string, userName string -//@return: err error - -func (jwtService *JwtService) SetRedisJWT(jwt string, userName string) (err error) { - // 此处过期时间等于jwt过期时间 - dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) - if err != nil { - return err - } - timer := dr - err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() - return err -} - func LoadAll() { var data []string err := global.GVA_DB.Model(&system.JwtBlacklist{}).Select("jwt").Find(&data).Error diff --git a/server/service/system/sys_casbin.go b/server/service/system/sys_casbin.go index 6b54904c0e..264c36aff6 100644 --- a/server/service/system/sys_casbin.go +++ b/server/service/system/sys_casbin.go @@ -3,17 +3,14 @@ package system import ( "errors" "strconv" - "sync" "gorm.io/gorm" - "github.com/casbin/casbin/v2" - "github.com/casbin/casbin/v2/model" gormadapter "github.com/casbin/gorm-adapter/v3" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" _ "github.com/go-sql-driver/mysql" - "go.uber.org/zap" ) //@author: [piexlmax](https://github.com/piexlmax) @@ -68,7 +65,7 @@ func (casbinService *CasbinService) UpdateCasbin(adminAuthorityID, AuthorityID u if len(rules) == 0 { return nil } // 设置空权限无需调用 AddPolicies 方法 - e := casbinService.Casbin() + e := utils.GetCasbin() success, _ := e.AddPolicies(rules) if !success { return errors.New("存在相同api,添加失败,请联系管理员") @@ -91,7 +88,7 @@ func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath stri return err } - e := casbinService.Casbin() + e := utils.GetCasbin() return e.LoadPolicy() } @@ -102,7 +99,7 @@ func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath stri //@return: pathMaps []request.CasbinInfo func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) (pathMaps []request.CasbinInfo) { - e := casbinService.Casbin() + e := utils.GetCasbin() authorityId := strconv.Itoa(int(AuthorityID)) list, _ := e.GetFilteredPolicy(0, authorityId) for _, v := range list { @@ -121,7 +118,7 @@ func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) //@return: bool func (casbinService *CasbinService) ClearCasbin(v int, p ...string) bool { - e := casbinService.Casbin() + e := utils.GetCasbin() success, _ := e.RemoveFilteredPolicy(v, p...) return success } @@ -170,52 +167,7 @@ func (casbinService *CasbinService) AddPolicies(db *gorm.DB, rules [][]string) e } func (casbinService *CasbinService) FreshCasbin() (err error) { - e := casbinService.Casbin() + e := utils.GetCasbin() err = e.LoadPolicy() return err } - -//@author: [piexlmax](https://github.com/piexlmax) -//@function: Casbin -//@description: 持久化到数据库 引入自定义规则 -//@return: *casbin.Enforcer - -var ( - syncedCachedEnforcer *casbin.SyncedCachedEnforcer - once sync.Once -) - -func (casbinService *CasbinService) Casbin() *casbin.SyncedCachedEnforcer { - once.Do(func() { - a, err := gormadapter.NewAdapterByDB(global.GVA_DB) - if err != nil { - zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err)) - return - } - text := ` - [request_definition] - r = sub, obj, act - - [policy_definition] - p = sub, obj, act - - [role_definition] - g = _, _ - - [policy_effect] - e = some(where (p.eft == allow)) - - [matchers] - m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act - ` - m, err := model.NewModelFromString(text) - if err != nil { - zap.L().Error("字符串加载模型失败!", zap.Error(err)) - return - } - syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) - syncedCachedEnforcer.SetExpireTime(60 * 60) - _ = syncedCachedEnforcer.LoadPolicy() - }) - return syncedCachedEnforcer -} diff --git a/server/service/system/sys_operation_record.go b/server/service/system/sys_operation_record.go index adfc25efd1..ef131db9ac 100644 --- a/server/service/system/sys_operation_record.go +++ b/server/service/system/sys_operation_record.go @@ -17,11 +17,6 @@ type OperationRecordService struct{} var OperationRecordServiceApp = new(OperationRecordService) -func (operationRecordService *OperationRecordService) CreateSysOperationRecord(sysOperationRecord system.SysOperationRecord) (err error) { - err = global.GVA_DB.Create(&sysOperationRecord).Error - return err -} - //@author: [granty1](https://github.com/granty1) //@author: [piexlmax](https://github.com/piexlmax) //@function: DeleteSysOperationRecordByIds diff --git a/server/utils/casbin_util.go b/server/utils/casbin_util.go new file mode 100644 index 0000000000..62d44fcfc0 --- /dev/null +++ b/server/utils/casbin_util.go @@ -0,0 +1,52 @@ +package utils + +import ( + "sync" + + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +var ( + syncedCachedEnforcer *casbin.SyncedCachedEnforcer + once sync.Once +) + +// GetCasbin 获取casbin实例 +func GetCasbin() *casbin.SyncedCachedEnforcer { + once.Do(func() { + a, err := gormadapter.NewAdapterByDB(global.GVA_DB) + if err != nil { + zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err)) + return + } + text := ` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act + ` + m, err := model.NewModelFromString(text) + if err != nil { + zap.L().Error("字符串加载模型失败!", zap.Error(err)) + return + } + syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) + syncedCachedEnforcer.SetExpireTime(60 * 60) + _ = syncedCachedEnforcer.LoadPolicy() + }) + return syncedCachedEnforcer +} diff --git a/server/utils/jwt.go b/server/utils/jwt.go index f34b792fa4..b4e6b3b2bb 100644 --- a/server/utils/jwt.go +++ b/server/utils/jwt.go @@ -1,6 +1,7 @@ package utils import ( + "context" "errors" "time" @@ -85,3 +86,20 @@ func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) { } return nil, TokenValid } + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetRedisJWT +//@description: jwt存入redis并设置过期时间 +//@param: jwt string, userName string +//@return: err error + +func SetRedisJWT(jwt string, userName string) (err error) { + // 此处过期时间等于jwt过期时间 + dr, err := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + return err + } + timer := dr + err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() + return err +} diff --git a/server/utils/system_events.go b/server/utils/system_events.go new file mode 100644 index 0000000000..736ea747f3 --- /dev/null +++ b/server/utils/system_events.go @@ -0,0 +1,34 @@ +package utils + +import ( + "sync" +) + +// SystemEvents 定义系统级事件处理 +type SystemEvents struct { + reloadHandlers []func() error + mu sync.RWMutex +} + +// 全局事件管理器 +var GlobalSystemEvents = &SystemEvents{} + +// RegisterReloadHandler 注册系统重载处理函数 +func (e *SystemEvents) RegisterReloadHandler(handler func() error) { + e.mu.Lock() + defer e.mu.Unlock() + e.reloadHandlers = append(e.reloadHandlers, handler) +} + +// TriggerReload 触发所有注册的重载处理函数 +func (e *SystemEvents) TriggerReload() error { + e.mu.RLock() + defer e.mu.RUnlock() + + for _, handler := range e.reloadHandlers { + if err := handler(); err != nil { + return err + } + } + return nil +} From e9234a8bdaf42678459fd6df5fbd73b9c7f23610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Mon, 21 Apr 2025 11:50:07 +0800 Subject: [PATCH 03/19] =?UTF-8?q?refactor(server):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=88=9D=E5=A7=8B=E5=8C=96=E5=92=8C=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `RunServer` 函数重命名为 `initServer`,并调整其调用方式以简化代码。同时,在系统初始化时添加 `SetupHandlers` 函数以注册全局处理函数,提升代码可维护性。 --- server/core/server.go | 7 +++---- server/core/server_run.go | 19 ++++--------------- server/initialize/init.go | 4 ++-- server/main.go | 1 + 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/server/core/server.go b/server/core/server.go index eccff7064f..9dd1c736ec 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -6,6 +6,7 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/initialize" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "go.uber.org/zap" + "time" ) func RunWindowsServer() { @@ -29,9 +30,6 @@ func RunWindowsServer() { Router := initialize.Routers() address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr) - s := initServer(address, Router) - - global.GVA_LOG.Info("server run success on ", zap.String("address", address)) fmt.Printf(` 欢迎使用 gin-vue-admin @@ -47,5 +45,6 @@ func RunWindowsServer() { ** 版权持有公司:北京翻转极光科技有限责任公司 ** ** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html ** `, address) - global.GVA_LOG.Error(s.ListenAndServe().Error()) + + initServer(address, Router, 10*time.Minute, 10*time.Minute) } diff --git a/server/core/server_run.go b/server/core/server_run.go index bd41e9c9ca..17e4cc0033 100644 --- a/server/core/server_run.go +++ b/server/core/server_run.go @@ -18,19 +18,8 @@ type server interface { Shutdown(context.Context) error } -// 初始化服务 -func initServer(address string, router *gin.Engine) server { - return &http.Server{ - Addr: address, - Handler: router, - ReadTimeout: 10 * time.Minute, - WriteTimeout: 10 * time.Minute, - MaxHeaderBytes: 1 << 20, - } -} - -// RunServer 启动服务并实现优雅关闭 -func RunServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { +// initServer 启动服务并实现优雅关闭 +func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { // 创建服务 srv := &http.Server{ Addr: address, @@ -61,10 +50,10 @@ func RunServer(address string, router *gin.Engine, readTimeout, writeTimeout tim // 设置5秒的超时时间 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - + if err := srv.Shutdown(ctx); err != nil { zap.L().Fatal("服务器关闭异常", zap.Error(err)) } - + zap.L().Info("服务器已优雅关闭") } diff --git a/server/initialize/init.go b/server/initialize/init.go index f4955cbda2..4dc48f3160 100644 --- a/server/initialize/init.go +++ b/server/initialize/init.go @@ -6,8 +6,8 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/utils" ) -// 初始化函数,在应用启动时调用 -func SetupReloadHandlers() { +// 初始化全局函数 +func SetupHandlers() { // 注册系统重载处理函数 utils.GlobalSystemEvents.RegisterReloadHandler(func() error { return Reload() diff --git a/server/main.go b/server/main.go index 8e27a39894..84c3d38941 100644 --- a/server/main.go +++ b/server/main.go @@ -45,6 +45,7 @@ func initializeSystem() { global.GVA_DB = initialize.Gorm() // gorm连接数据库 initialize.Timer() initialize.DBList() + initialize.SetupHandlers() // 注册全局函数 if global.GVA_DB != nil { initialize.RegisterTables() // 初始化表 } From e6db46fd1f406f1d7e2d21e45006c6791b16f7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Mon, 21 Apr 2025 15:03:06 +0800 Subject: [PATCH 04/19] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8C=96=E4=BB=A3=E7=A0=81enum=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/utils/autocode/template_funcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/autocode/template_funcs.go b/server/utils/autocode/template_funcs.go index 26b1d272d5..8a88ac7ab3 100644 --- a/server/utils/autocode/template_funcs.go +++ b/server/utils/autocode/template_funcs.go @@ -127,7 +127,7 @@ func GenerateSearchConditions(fields []*systemReq.AutoCodeField) string { } else { condition = fmt.Sprintf(` if info.%s != "" { - db = db.Where("%s %s ?", *info.%s) + db = db.Where("%s %s ?", info.%s) }`, field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName) } From 19fd16de968eb103f4ab9451c6acfe1fee33d365 Mon Sep 17 00:00:00 2001 From: Gor-c Date: Mon, 28 Apr 2025 14:14:37 +0800 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BB=84?= =?UTF-8?q?=E5=90=88=E6=A8=A1=E5=BC=8F=E4=B8=8B=EF=BC=8C=E9=A1=B6=E9=83=A8?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E9=87=8D=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pinia/modules/router.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/pinia/modules/router.js b/web/src/pinia/modules/router.js index db8c5150d7..4ee54891c6 100644 --- a/web/src/pinia/modules/router.js +++ b/web/src/pinia/modules/router.js @@ -101,6 +101,8 @@ export const useRouterStore = defineStore('router', () => { watchEffect(() => { let topActive = sessionStorage.getItem('topActive') asyncRouters.value[0]?.children.forEach((item) => { + // 初始化菜单内容,防止重复添加 + topMenu.value = []; if (item.hidden) return menuMap[item.name] = item topMenu.value.push({ ...item, children: [] }) From 3a84455468d42fa96d643683265e245e469ae27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Mon, 28 Apr 2025 16:02:01 +0800 Subject: [PATCH 06/19] =?UTF-8?q?refactor:=20=E4=BF=AE=E6=94=B9=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=20RunWindowsServer=20=3D=3D>=20RunServer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/core/server.go | 2 +- server/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/core/server.go b/server/core/server.go index 0a44539206..389ab9485b 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -9,7 +9,7 @@ import ( "time" ) -func RunWindowsServer() { +func RunServer() { if global.GVA_CONFIG.System.UseRedis { // 初始化redis服务 initialize.Redis() diff --git a/server/main.go b/server/main.go index 84c3d38941..ff8d1b5c1f 100644 --- a/server/main.go +++ b/server/main.go @@ -32,7 +32,7 @@ func main() { initializeSystem() // 运行服务器 - core.RunWindowsServer() + core.RunServer() } // initializeSystem 初始化系统所有组件 From da0eb2f98e8cd69676ed804a730351504caffb23 Mon Sep 17 00:00:00 2001 From: QIN xiansheng Date: Tue, 29 Apr 2025 11:37:47 +0800 Subject: [PATCH 07/19] =?UTF-8?q?=E6=96=B0=E5=A2=9Emcp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/go.mod | 7 +- server/go.sum | 14 ++- server/initialize/mcp/mcp_server.go | 91 +++++++++++++++++ server/model/mcp/model.go | 20 ++++ server/service/mcp/client/client.go | 102 +++++++++++++++++++ server/service/mcp/enter.go | 11 ++ server/service/mcp/service/enter.go | 5 + server/service/mcp/service/service.go | 30 ++++++ server/service/mcp/service/tool/enter.go | 6 ++ server/service/mcp/service/tool/tool_call.go | 51 ++++++++++ server/service/mcp/service/tool/tool_list.go | 23 +++++ 11 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 server/initialize/mcp/mcp_server.go create mode 100644 server/model/mcp/model.go create mode 100644 server/service/mcp/client/client.go create mode 100644 server/service/mcp/enter.go create mode 100644 server/service/mcp/service/enter.go create mode 100644 server/service/mcp/service/service.go create mode 100644 server/service/mcp/service/tool/enter.go create mode 100644 server/service/mcp/service/tool/tool_call.go create mode 100644 server/service/mcp/service/tool/tool_list.go diff --git a/server/go.mod b/server/go.mod index 157477a265..3cd6371eaf 100644 --- a/server/go.mod +++ b/server/go.mod @@ -3,12 +3,12 @@ module github.com/flipped-aurora/gin-vue-admin/server go 1.22.2 require ( + github.com/ThinkInAIXYZ/go-mcp v0.2.2 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aws/aws-sdk-go v1.55.6 github.com/casbin/casbin/v2 v2.103.0 github.com/casbin/gorm-adapter/v3 v3.32.0 github.com/fsnotify/fsnotify v1.8.0 - github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 github.com/gin-gonic/gin v1.10.0 github.com/glebarez/sqlite v1.11.0 github.com/go-sql-driver/mysql v1.8.1 @@ -122,6 +122,7 @@ require ( github.com/mozillazg/go-httpheader v0.4.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nwaples/rardecode/v2 v2.0.1 // indirect + github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect github.com/otiai10/mint v1.6.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect @@ -141,6 +142,9 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -152,6 +156,7 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/server/go.sum b/server/go.sum index 403ba70790..4d6ff340f5 100644 --- a/server/go.sum +++ b/server/go.sum @@ -48,6 +48,8 @@ github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6Xge github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= github.com/STARRY-S/zip v0.1.0 h1:eUER3jKmHKXjv+iy3BekLa+QnNSo1Lqz4eTzYBcGDqo= github.com/STARRY-S/zip v0.1.0/go.mod h1:qj/mTZkvb3AvfGQ2e775/3AODRvB4peSw8KNMvrM8/I= +github.com/ThinkInAIXYZ/go-mcp v0.2.2 h1:zlm4Xo8pxGzmfTvN16hM0YP75Q7QZ713g3mZSbO3JAs= +github.com/ThinkInAIXYZ/go-mcp v0.2.2/go.mod h1:KnUWUymko7rmOgzvIjxwX0uB9oiJeLF/Q3W9cRt8fVg= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= @@ -113,8 +115,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= -github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= @@ -349,6 +349,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nwaples/rardecode/v2 v2.0.1 h1:3MN6/R+Y4c7e+21U3yhWuUcf72sYmcmr6jtiuAVSH1A= github.com/nwaples/rardecode/v2 v2.0.1/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= +github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= @@ -449,6 +451,12 @@ github.com/tencentyun/cos-go-sdk-v5 v0.7.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72 github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= @@ -478,6 +486,8 @@ github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/server/initialize/mcp/mcp_server.go b/server/initialize/mcp/mcp_server.go new file mode 100644 index 0000000000..2e36d1e095 --- /dev/null +++ b/server/initialize/mcp/mcp_server.go @@ -0,0 +1,91 @@ +package mcp + +import ( + "context" + "fmt" + "time" + + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "github.com/ThinkInAIXYZ/go-mcp/server" + "github.com/ThinkInAIXYZ/go-mcp/transport" + + model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" + McpTool "github.com/flipped-aurora/gin-vue-admin/server/service/mcp/service/tool" +) + +type McpServer struct{} + +var mcpServer *server.Server + +type tool struct { + ListTool func() (name string, description string, inputReqStruct interface{}) + CallTool func(context.Context, *protocol.CallToolRequest) (*protocol.CallToolResult, error) +} + +var ( + ToolCall = new(McpTool.ToolCall) + ToolList = new(McpTool.ToolList) +) + +func (s *McpServer) NewSSE(Config *model.McpConfig) error { + transportServer, err := transport.NewSSEServerTransport(Config.Url) + if err != nil { + return fmt.Errorf("创建mcp服务器失败: %v", err) + } + mcpServer, err = server.NewServer(transportServer, + server.WithServerInfo(protocol.Implementation{ + Name: Config.Name, + Version: Config.Version, + }), + ) + if err != nil { + return fmt.Errorf("创建mcp服务器失败: %v", err) + } + return nil +} + +func (s *McpServer) BinTool() { + if mcpServer == nil { + fmt.Println("mcp服务器: MCP服务器未初始化") + return + } + + b := []tool{ + {ToolList.CityWeather, ToolCall.CallCityWeather}, + {ToolList.CurrentTime, ToolCall.CallCurrentTime}, + } + for _, v := range b { + s.RegisterTool(v) + } + fmt.Println("mcp服务器: 注册工具成功 ") + +} + +func (s *McpServer) RegisterTool(B tool) { + if mcpServer == nil { + fmt.Println("mcp服务器: MCP服务器未初始化") + return + } + + tt, err := protocol.NewTool(B.ListTool()) + if err != nil { + panic(fmt.Errorf("创建mcp工具失败: %v", err)) + } + + mcpServer.RegisterTool(tt, B.CallTool) +} + +func (s *McpServer) Run() (err error) { + + //启动服务器 + go func() { + err = mcpServer.Run() + }() + + time.Sleep(1000 * time.Millisecond) + if mcpServer == nil || err != nil { + return fmt.Errorf("mcp服务器: MCP服务器未初始化") + } + fmt.Println("mcp服务器: 启动成功 ") + select {} // 阻塞主线程,保持运行 +} diff --git a/server/model/mcp/model.go b/server/model/mcp/model.go new file mode 100644 index 0000000000..ce20fe4ec8 --- /dev/null +++ b/server/model/mcp/model.go @@ -0,0 +1,20 @@ +package model + +type McpConfig struct { + Url string + Name string + Version string +} + +// 获取当前系统时间 +type CurrentTimeReq struct { + Timezone string `json:"timezone" description:"current time timezone"` +} + +// 获取天气信息 +// 参数:城市名称 +// 日期: 2023-07-01 +type WeatherReq struct { + City string `json:"city" description:"city name"` + Date string `json:"date" description:"date"` +} diff --git a/server/service/mcp/client/client.go b/server/service/mcp/client/client.go new file mode 100644 index 0000000000..82a36df85a --- /dev/null +++ b/server/service/mcp/client/client.go @@ -0,0 +1,102 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "time" + + model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" + + "github.com/ThinkInAIXYZ/go-mcp/client" + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "github.com/ThinkInAIXYZ/go-mcp/transport" +) + +type ClientStruct struct{} + +var mcpClient *client.Client + +// 链接mcp服务器 +func (c *ClientStruct) NewSSE(Config *model.McpConfig) error { + transportClient, err := transport.NewSSEClientTransport(Config.Url) + if err != nil { + return fmt.Errorf("创建mcp客户端失败SSE: %v", err) + } + mcpClient, err = client.NewClient(transportClient, client.WithClientInfo(protocol.Implementation{ + Name: Config.Name, + Version: Config.Version, + })) + if err != nil { + return fmt.Errorf("创建mcp客户端失败: %v", err) + } + defer mcpClient.Close() + fmt.Println("MCP客户端已启动") + + return nil +} + +// 列出所有可用工具 +func (c *ClientStruct) ListTools() (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + toolsResult, err := mcpClient.ListTools(ctx) + if err != nil { + return "", fmt.Errorf("获取工具列表失败: %v", err) + + } + b, _ := json.Marshal(toolsResult.Tools) + return string(b), nil +} + +// 调用工具 +func (c *ClientStruct) CallTool(toolName string, inputReq map[string]interface{}) (string, error) { + input := make(map[string]interface{}) + for k, v := range inputReq { + input[k] = v + } + + callToolResult, err := mcpClient.CallTool(context.Background(), + protocol.NewCallToolRequest(toolName, input)) + if err != nil { + return "", fmt.Errorf("使用工具失败: %v", err) + } + b, _ := json.Marshal(callToolResult) + return string(b), nil +} + +// 启动mcp client +func (c *ClientStruct) Run() { + + //连接McpServe服务器 + err := c.NewSSE(&model.McpConfig{ + Url: "http://127.0.0.1:9999/sse", + Name: "mcpClient", + Version: "1.0.0", + }) + if err != nil || mcpClient == nil { + fmt.Println("连接McpServe服务器失败") + return + } + + fmt.Println("McpServe服务器已启动") + + //(示例) + { + // 列出所有工具(示例) + tools, err := c.ListTools() + if err != nil { + fmt.Println("获取工具列表失败:", err) + return + } + fmt.Println("工具列表:", tools) + + // 调用工具(示例) + result, err := c.CallTool("CurrentTime", nil) + if err != nil { + fmt.Println("使用工具失败:", err) + return + } + fmt.Println("工具结果:", result) + } +} diff --git a/server/service/mcp/enter.go b/server/service/mcp/enter.go new file mode 100644 index 0000000000..dd9bee53d9 --- /dev/null +++ b/server/service/mcp/enter.go @@ -0,0 +1,11 @@ +package mcp + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/service/mcp/client" + "github.com/flipped-aurora/gin-vue-admin/server/service/mcp/service" +) + +type McpGroup struct { + ServiceGroup service.McpServiceGroup + ClientGroup client.ClientStruct +} diff --git a/server/service/mcp/service/enter.go b/server/service/mcp/service/enter.go new file mode 100644 index 0000000000..10b11b77b1 --- /dev/null +++ b/server/service/mcp/service/enter.go @@ -0,0 +1,5 @@ +package service + +type McpServiceGroup struct { + McpService +} diff --git a/server/service/mcp/service/service.go b/server/service/mcp/service/service.go new file mode 100644 index 0000000000..e63cd92f4c --- /dev/null +++ b/server/service/mcp/service/service.go @@ -0,0 +1,30 @@ +package service + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/initialize/mcp" + model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" +) + +type McpService struct { +} + +var Init = new(mcp.McpServer) + +func (m *McpService) Run() { + + config := &model.McpConfig{ + Url: `127.0.0.1:9999`, + Name: "mcpServer", + Version: "1.0.0", + } + + // 创建mcp服务器-sse方式 + Init.NewSSE(config) + + // 注册工具 + Init.BinTool() + + // 启动mcp服务器 + Init.Run() + +} diff --git a/server/service/mcp/service/tool/enter.go b/server/service/mcp/service/tool/enter.go new file mode 100644 index 0000000000..56d552321d --- /dev/null +++ b/server/service/mcp/service/tool/enter.go @@ -0,0 +1,6 @@ +package tool + +type ToolGroup struct { + ToolList + ToolCall +} diff --git a/server/service/mcp/service/tool/tool_call.go b/server/service/mcp/service/tool/tool_call.go new file mode 100644 index 0000000000..1a18c29157 --- /dev/null +++ b/server/service/mcp/service/tool/tool_call.go @@ -0,0 +1,51 @@ +package tool + +import ( + "context" + "encoding/json" + "time" + + "github.com/ThinkInAIXYZ/go-mcp/protocol" + model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" +) + +type ToolCall struct{} + +// 获取当前系统时间 +func (t *ToolCall) CallCurrentTime(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { + // 获取当前系统时间 + currentTime := time.Now().Format("2006-01-02 15:04:05") + //返回 + return &protocol.CallToolResult{ + Content: []protocol.Content{ + protocol.TextContent{ + Type: "text", + Text: currentTime, + }, + }, + }, nil +} + +// 获取天气信息 +// 参数:城市名称 +// 日期: 2023-07-01 + +func (t *ToolCall) CallCityWeather(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { + // 解析请求参数 + req := new(model.WeatherReq) + if err := json.Unmarshal(request.RawArguments, &req); err != nil { + return nil, err + } + + //构造回复信息 + str := "天气信息: " + req.Date + " " + req.City + " 天气晴朗,温度为 25°C,相对湿度为 60%,风速为 10m/s。" + + return &protocol.CallToolResult{ + Content: []protocol.Content{ + protocol.TextContent{ + Type: "text", + Text: str, + }, + }, + }, nil +} diff --git a/server/service/mcp/service/tool/tool_list.go b/server/service/mcp/service/tool/tool_list.go new file mode 100644 index 0000000000..1ba2755228 --- /dev/null +++ b/server/service/mcp/service/tool/tool_list.go @@ -0,0 +1,23 @@ +package tool + +import ( + model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" +) + +type ToolList struct{} + +// 获取当前系统时间 +func (t *ToolList) CurrentTime() (name string, description string, inputReqStruct interface{}) { + name = "CurrentTime" + description = "获取当前系统时间" + inputReqStruct = model.CurrentTimeReq{} + return +} + +// 获取天气 +func (t *ToolList) CityWeather() (name string, description string, inputReqStruct interface{}) { + name = "Weather" + description = "获取天气" + inputReqStruct = model.WeatherReq{} //自动解析参数 + return +} From 761927003b59668dda5311a41e1497740fe9ac89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Thu, 8 May 2025 18:14:57 +0800 Subject: [PATCH 08/19] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81mcp=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/config.yaml | 8 +- server/config/config.go | 3 + server/config/mcp.go | 9 ++ server/core/server.go | 9 +- server/core/server_run.go | 18 +++- server/initialize/mcp/mcp_server.go | 91 ----------------- server/initialize/router.go | 21 +++- server/main.go | 1 - server/mcp/client/main.go | 31 ++++++ server/mcp/main.go | 39 +++++++ server/mcp/tool/current_time/handle.go | 22 ++++ server/mcp/tool/current_time/model.go | 7 ++ server/mcp/tool/current_time/new.go | 9 ++ server/mcp/tool/enter.go | 43 ++++++++ server/mcp/tool/weather/handle.go | 28 +++++ server/mcp/tool/weather/model.go | 8 ++ server/mcp/tool/weather/new.go | 9 ++ server/model/mcp/model.go | 20 ---- server/service/mcp/client/client.go | 102 ------------------- server/service/mcp/enter.go | 11 -- server/service/mcp/service/enter.go | 5 - server/service/mcp/service/service.go | 30 ------ server/service/mcp/service/tool/enter.go | 6 -- server/service/mcp/service/tool/tool_call.go | 51 ---------- server/service/mcp/service/tool/tool_list.go | 23 ----- 25 files changed, 253 insertions(+), 351 deletions(-) create mode 100644 server/config/mcp.go delete mode 100644 server/initialize/mcp/mcp_server.go create mode 100644 server/mcp/client/main.go create mode 100644 server/mcp/main.go create mode 100644 server/mcp/tool/current_time/handle.go create mode 100644 server/mcp/tool/current_time/model.go create mode 100644 server/mcp/tool/current_time/new.go create mode 100644 server/mcp/tool/enter.go create mode 100644 server/mcp/tool/weather/handle.go create mode 100644 server/mcp/tool/weather/model.go create mode 100644 server/mcp/tool/weather/new.go delete mode 100644 server/model/mcp/model.go delete mode 100644 server/service/mcp/client/client.go delete mode 100644 server/service/mcp/enter.go delete mode 100644 server/service/mcp/service/enter.go delete mode 100644 server/service/mcp/service/service.go delete mode 100644 server/service/mcp/service/tool/enter.go delete mode 100644 server/service/mcp/service/tool/tool_call.go delete mode 100644 server/service/mcp/service/tool/tool_list.go diff --git a/server/config.yaml b/server/config.yaml index c3ef828809..6651257fe4 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -274,4 +274,10 @@ cors: allow-headers: content-type allow-methods: GET, POST expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true # 布尔值 \ No newline at end of file + allow-credentials: true # 布尔值 +mcp: + name: gin-vue-admin MCP服务 + version: v1.0.0 + sse_path: /sse + message_path: /message + url_prefix: '' diff --git a/server/config/config.go b/server/config/config.go index 15da9c7db4..3abac5ac03 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -34,4 +34,7 @@ type Server struct { // 跨域配置 Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"` + + // MCP配置 + MCP MCP `mapstructure:"mcp" json:"mcp" yaml:"mcp"` } diff --git a/server/config/mcp.go b/server/config/mcp.go new file mode 100644 index 0000000000..81f4bff307 --- /dev/null +++ b/server/config/mcp.go @@ -0,0 +1,9 @@ +package config + +type MCP struct { + Name string `mapstructure:"name" json:"name" yaml:"name"` // MCP名称 + Version string `mapstructure:"version" json:"version" yaml:"version"` // MCP版本 + SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"` // SSE路径 + MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"` // 消息路径 + UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"` // URL前缀 +} diff --git a/server/core/server.go b/server/core/server.go index 389ab9485b..68eb927db1 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -29,7 +29,7 @@ func RunServer() { system.LoadAll() } - Router := initialize.Routers() + Router, mcpServer := initialize.Routers() address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr) @@ -41,12 +41,13 @@ func RunServer() { 插件市场:https://plugin.gin-vue-admin.com GVA讨论社区:https://support.qq.com/products/371961 默认自动化文档地址:http://127.0.0.1%s/swagger/index.html + 默认MCP SSE地址:http://127.0.0.1%s%s + 默认MCP Message地址:http://127.0.0.1%s%s 默认前端文件运行地址:http://127.0.0.1:8080 --------------------------------------版权声明-------------------------------------- ** 版权所有方:flipped-aurora开源团队 ** ** 版权持有公司:北京翻转极光科技有限责任公司 ** ** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html ** -`, address) - - initServer(address, Router, 10*time.Minute, 10*time.Minute) +`, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath) + initServer(address, Router, mcpServer, 10*time.Minute, 10*time.Minute) } diff --git a/server/core/server_run.go b/server/core/server_run.go index 17e4cc0033..14e8a43a5f 100644 --- a/server/core/server_run.go +++ b/server/core/server_run.go @@ -3,6 +3,7 @@ package core import ( "context" "fmt" + "github.com/ThinkInAIXYZ/go-mcp/transport" "net/http" "os" "os/signal" @@ -19,7 +20,7 @@ type server interface { } // initServer 启动服务并实现优雅关闭 -func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { +func initServer(address string, router *gin.Engine, mcpServer transport.ServerTransport, readTimeout, writeTimeout time.Duration) { // 创建服务 srv := &http.Server{ Addr: address, @@ -45,15 +46,24 @@ func initServer(address string, router *gin.Engine, readTimeout, writeTimeout ti // kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - zap.L().Info("关闭服务器...") + zap.L().Info("关闭WEB服务...") // 设置5秒的超时时间 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() if err := srv.Shutdown(ctx); err != nil { - zap.L().Fatal("服务器关闭异常", zap.Error(err)) + zap.L().Fatal("WEB服务关闭异常", zap.Error(err)) + } + + zap.L().Info("WEB服务已关闭") + + zap.L().Info("关闭MCP服务...") + + if err := mcpServer.Shutdown(ctx, ctx); err != nil { + zap.L().Fatal("MCP服务器关闭异常", zap.Error(err)) } - zap.L().Info("服务器已优雅关闭") + zap.L().Info("MCP服务器已关闭") } diff --git a/server/initialize/mcp/mcp_server.go b/server/initialize/mcp/mcp_server.go deleted file mode 100644 index 2e36d1e095..0000000000 --- a/server/initialize/mcp/mcp_server.go +++ /dev/null @@ -1,91 +0,0 @@ -package mcp - -import ( - "context" - "fmt" - "time" - - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "github.com/ThinkInAIXYZ/go-mcp/server" - "github.com/ThinkInAIXYZ/go-mcp/transport" - - model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" - McpTool "github.com/flipped-aurora/gin-vue-admin/server/service/mcp/service/tool" -) - -type McpServer struct{} - -var mcpServer *server.Server - -type tool struct { - ListTool func() (name string, description string, inputReqStruct interface{}) - CallTool func(context.Context, *protocol.CallToolRequest) (*protocol.CallToolResult, error) -} - -var ( - ToolCall = new(McpTool.ToolCall) - ToolList = new(McpTool.ToolList) -) - -func (s *McpServer) NewSSE(Config *model.McpConfig) error { - transportServer, err := transport.NewSSEServerTransport(Config.Url) - if err != nil { - return fmt.Errorf("创建mcp服务器失败: %v", err) - } - mcpServer, err = server.NewServer(transportServer, - server.WithServerInfo(protocol.Implementation{ - Name: Config.Name, - Version: Config.Version, - }), - ) - if err != nil { - return fmt.Errorf("创建mcp服务器失败: %v", err) - } - return nil -} - -func (s *McpServer) BinTool() { - if mcpServer == nil { - fmt.Println("mcp服务器: MCP服务器未初始化") - return - } - - b := []tool{ - {ToolList.CityWeather, ToolCall.CallCityWeather}, - {ToolList.CurrentTime, ToolCall.CallCurrentTime}, - } - for _, v := range b { - s.RegisterTool(v) - } - fmt.Println("mcp服务器: 注册工具成功 ") - -} - -func (s *McpServer) RegisterTool(B tool) { - if mcpServer == nil { - fmt.Println("mcp服务器: MCP服务器未初始化") - return - } - - tt, err := protocol.NewTool(B.ListTool()) - if err != nil { - panic(fmt.Errorf("创建mcp工具失败: %v", err)) - } - - mcpServer.RegisterTool(tt, B.CallTool) -} - -func (s *McpServer) Run() (err error) { - - //启动服务器 - go func() { - err = mcpServer.Run() - }() - - time.Sleep(1000 * time.Millisecond) - if mcpServer == nil || err != nil { - return fmt.Errorf("mcp服务器: MCP服务器未初始化") - } - fmt.Println("mcp服务器: 启动成功 ") - select {} // 阻塞主线程,保持运行 -} diff --git a/server/initialize/router.go b/server/initialize/router.go index c3dfe1ebb3..cb9beee669 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -1,6 +1,9 @@ package initialize import ( + "github.com/ThinkInAIXYZ/go-mcp/transport" + "github.com/flipped-aurora/gin-vue-admin/server/mcp" + "go.uber.org/zap" "net/http" "os" @@ -33,13 +36,27 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) { // 初始化总路由 -func Routers() *gin.Engine { +func Routers() (*gin.Engine, transport.ServerTransport) { Router := gin.New() Router.Use(gin.Recovery()) if gin.Mode() == gin.DebugMode { Router.Use(gin.Logger()) } + mcpServer, mcpHandler, err := mcp.Run() + if err != nil { + global.GVA_LOG.Error("创建MCP服务器失败", zap.Error(err)) + } + + // 注册mcp服务 + Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) { + mcpHandler.HandleSSE().ServeHTTP(c.Writer, c.Request) + }) + + Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) { + mcpHandler.HandleMessage().ServeHTTP(c.Writer, c.Request) + }) + systemRouter := router.RouterGroupApp.System exampleRouter := router.RouterGroupApp.Example // 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的 @@ -107,5 +124,5 @@ func Routers() *gin.Engine { global.GVA_ROUTERS = Router.Routes() global.GVA_LOG.Info("router register success") - return Router + return Router, mcpServer } diff --git a/server/main.go b/server/main.go index ff8d1b5c1f..a079ca94ae 100644 --- a/server/main.go +++ b/server/main.go @@ -30,7 +30,6 @@ import ( func main() { // 初始化系统 initializeSystem() - // 运行服务器 core.RunServer() } diff --git a/server/mcp/client/main.go b/server/mcp/client/main.go new file mode 100644 index 0000000000..e75c139832 --- /dev/null +++ b/server/mcp/client/main.go @@ -0,0 +1,31 @@ +package client + +import ( + "fmt" + "github.com/ThinkInAIXYZ/go-mcp/client" + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "github.com/ThinkInAIXYZ/go-mcp/transport" +) + +type Client struct{} + +var mcpClient *client.Client + +// 链接mcp服务器 +func (c *Client) New() error { + transportClient, err := transport.NewSSEClientTransport("http://localhost:8888/sse") + if err != nil { + return fmt.Errorf("创建mcp客户端失败SSE: %v", err) + } + mcpClient, err = client.NewClient(transportClient, client.WithClientInfo(protocol.Implementation{ + Name: "client", + Version: "v0.0.1", + })) + if err != nil { + return fmt.Errorf("创建mcp客户端失败: %v", err) + } + defer mcpClient.Close() + fmt.Println("MCP客户端已启动") + + return nil +} diff --git a/server/mcp/main.go b/server/mcp/main.go new file mode 100644 index 0000000000..b745889470 --- /dev/null +++ b/server/mcp/main.go @@ -0,0 +1,39 @@ +package mcp + +import ( + "fmt" + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "github.com/ThinkInAIXYZ/go-mcp/server" + "github.com/ThinkInAIXYZ/go-mcp/transport" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/mcp/tool" +) + +func Run() (transport.ServerTransport, *transport.SSEHandler, error) { + // 初始化SSE服务器 + config := global.GVA_CONFIG.MCP + transportServer, mcpHandle, err := transport.NewSSEServerTransportAndHandler(config.MessagePath, + transport.WithSSEServerTransportAndHandlerOptionLogger(global.GVA_LOG.Sugar()), + ) + if err != nil { + return nil, nil, fmt.Errorf("创建SSE服务器失败: %v", err) + } + mcpServer, err := server.NewServer(transportServer, + server.WithServerInfo(protocol.Implementation{ + Name: config.Name, + Version: config.Version, + }), + ) + if err != nil { + return nil, nil, fmt.Errorf("创建MCP服务器失败: %v", err) + } + + tool.RegisterAllTools(mcpServer) + + //启动服务器 + go func() { + err = mcpServer.Run() + }() + + return transportServer, mcpHandle, err +} diff --git a/server/mcp/tool/current_time/handle.go b/server/mcp/tool/current_time/handle.go new file mode 100644 index 0000000000..9157bc15d0 --- /dev/null +++ b/server/mcp/tool/current_time/handle.go @@ -0,0 +1,22 @@ +package current_time + +import ( + "context" + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "time" +) + +// 获取当前系统时间 +func (t *Tool) Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { + // 获取当前系统时间 + currentTime := time.Now().Format("2006-01-02 15:04:05") + //返回 + return &protocol.CallToolResult{ + Content: []protocol.Content{ + protocol.TextContent{ + Type: "text", + Text: currentTime, + }, + }, + }, nil +} diff --git a/server/mcp/tool/current_time/model.go b/server/mcp/tool/current_time/model.go new file mode 100644 index 0000000000..bd71c82413 --- /dev/null +++ b/server/mcp/tool/current_time/model.go @@ -0,0 +1,7 @@ +package current_time + +type Tool struct{} + +type Req struct { + Timezone string `json:"timezone" description:"current time timezone"` +} diff --git a/server/mcp/tool/current_time/new.go b/server/mcp/tool/current_time/new.go new file mode 100644 index 0000000000..23d78db9df --- /dev/null +++ b/server/mcp/tool/current_time/new.go @@ -0,0 +1,9 @@ +package current_time + +// 获取当前系统时间 +func (t *Tool) New() (name string, description string, inputReqStruct interface{}) { + name = "CurrentTime" + description = "获取当前系统时间" + inputReqStruct = t + return +} diff --git a/server/mcp/tool/enter.go b/server/mcp/tool/enter.go new file mode 100644 index 0000000000..beb1a0746a --- /dev/null +++ b/server/mcp/tool/enter.go @@ -0,0 +1,43 @@ +package tool + +import ( + "context" + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "github.com/ThinkInAIXYZ/go-mcp/server" + "github.com/flipped-aurora/gin-vue-admin/server/mcp/tool/current_time" + "github.com/flipped-aurora/gin-vue-admin/server/mcp/tool/weather" +) + +func init() { + // 注册工具 + RegisterTool(¤t_time.Tool{}) + RegisterTool(&weather.Tool{}) +} + +// McpTool 定义了MCP工具必须实现的接口 +type McpTool interface { + // Handle 返回工具调用信息 + Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) + // New 返回工具注册信息 + New() (name string, description string, inputReqStruct interface{}) +} + +// 工具注册表 +var toolRegister = make(map[string]McpTool) + +// RegisterTool 供工具在init时调用,将自己注册到工具注册表中 +func RegisterTool(tool McpTool) { + name, _, _ := tool.New() + toolRegister[name] = tool +} + +// RegisterAllTools 将所有注册的工具注册到MCP服务中 +func RegisterAllTools(mcpServer *server.Server) { + for _, tool := range toolRegister { + t, e := protocol.NewTool(tool.New()) + if e != nil { + panic("tool register error: " + e.Error()) + } + mcpServer.RegisterTool(t, tool.Handle) + } +} diff --git a/server/mcp/tool/weather/handle.go b/server/mcp/tool/weather/handle.go new file mode 100644 index 0000000000..c10d285c2f --- /dev/null +++ b/server/mcp/tool/weather/handle.go @@ -0,0 +1,28 @@ +package weather + +import ( + "context" + "encoding/json" + "github.com/ThinkInAIXYZ/go-mcp/protocol" +) + +// 获取天气 +func (t *Tool) Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { + var req Request + // 解析请求参数 + if err := json.Unmarshal(request.RawArguments, &req); err != nil { + return nil, err + } + + //构造回复信息 + str := "天气信息: " + req.Date + " " + req.City + " 天气晴朗,温度为 25°C,相对湿度为 60%,风速为 10m/s。" + + return &protocol.CallToolResult{ + Content: []protocol.Content{ + protocol.TextContent{ + Type: "text", + Text: str, + }, + }, + }, nil +} diff --git a/server/mcp/tool/weather/model.go b/server/mcp/tool/weather/model.go new file mode 100644 index 0000000000..f06438bbe9 --- /dev/null +++ b/server/mcp/tool/weather/model.go @@ -0,0 +1,8 @@ +package weather + +type Tool struct{} + +type Request struct { + City string `json:"city" description:"city name"` + Date string `json:"date" description:"date"` +} diff --git a/server/mcp/tool/weather/new.go b/server/mcp/tool/weather/new.go new file mode 100644 index 0000000000..5211a187d5 --- /dev/null +++ b/server/mcp/tool/weather/new.go @@ -0,0 +1,9 @@ +package weather + +// 获取天气 +func (t *Tool) New() (name string, description string, inputReqStruct interface{}) { + name = "Weather" + description = "获取天气" + inputReqStruct = t //自动解析参数 + return +} diff --git a/server/model/mcp/model.go b/server/model/mcp/model.go deleted file mode 100644 index ce20fe4ec8..0000000000 --- a/server/model/mcp/model.go +++ /dev/null @@ -1,20 +0,0 @@ -package model - -type McpConfig struct { - Url string - Name string - Version string -} - -// 获取当前系统时间 -type CurrentTimeReq struct { - Timezone string `json:"timezone" description:"current time timezone"` -} - -// 获取天气信息 -// 参数:城市名称 -// 日期: 2023-07-01 -type WeatherReq struct { - City string `json:"city" description:"city name"` - Date string `json:"date" description:"date"` -} diff --git a/server/service/mcp/client/client.go b/server/service/mcp/client/client.go deleted file mode 100644 index 82a36df85a..0000000000 --- a/server/service/mcp/client/client.go +++ /dev/null @@ -1,102 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "time" - - model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" - - "github.com/ThinkInAIXYZ/go-mcp/client" - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "github.com/ThinkInAIXYZ/go-mcp/transport" -) - -type ClientStruct struct{} - -var mcpClient *client.Client - -// 链接mcp服务器 -func (c *ClientStruct) NewSSE(Config *model.McpConfig) error { - transportClient, err := transport.NewSSEClientTransport(Config.Url) - if err != nil { - return fmt.Errorf("创建mcp客户端失败SSE: %v", err) - } - mcpClient, err = client.NewClient(transportClient, client.WithClientInfo(protocol.Implementation{ - Name: Config.Name, - Version: Config.Version, - })) - if err != nil { - return fmt.Errorf("创建mcp客户端失败: %v", err) - } - defer mcpClient.Close() - fmt.Println("MCP客户端已启动") - - return nil -} - -// 列出所有可用工具 -func (c *ClientStruct) ListTools() (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - toolsResult, err := mcpClient.ListTools(ctx) - if err != nil { - return "", fmt.Errorf("获取工具列表失败: %v", err) - - } - b, _ := json.Marshal(toolsResult.Tools) - return string(b), nil -} - -// 调用工具 -func (c *ClientStruct) CallTool(toolName string, inputReq map[string]interface{}) (string, error) { - input := make(map[string]interface{}) - for k, v := range inputReq { - input[k] = v - } - - callToolResult, err := mcpClient.CallTool(context.Background(), - protocol.NewCallToolRequest(toolName, input)) - if err != nil { - return "", fmt.Errorf("使用工具失败: %v", err) - } - b, _ := json.Marshal(callToolResult) - return string(b), nil -} - -// 启动mcp client -func (c *ClientStruct) Run() { - - //连接McpServe服务器 - err := c.NewSSE(&model.McpConfig{ - Url: "http://127.0.0.1:9999/sse", - Name: "mcpClient", - Version: "1.0.0", - }) - if err != nil || mcpClient == nil { - fmt.Println("连接McpServe服务器失败") - return - } - - fmt.Println("McpServe服务器已启动") - - //(示例) - { - // 列出所有工具(示例) - tools, err := c.ListTools() - if err != nil { - fmt.Println("获取工具列表失败:", err) - return - } - fmt.Println("工具列表:", tools) - - // 调用工具(示例) - result, err := c.CallTool("CurrentTime", nil) - if err != nil { - fmt.Println("使用工具失败:", err) - return - } - fmt.Println("工具结果:", result) - } -} diff --git a/server/service/mcp/enter.go b/server/service/mcp/enter.go deleted file mode 100644 index dd9bee53d9..0000000000 --- a/server/service/mcp/enter.go +++ /dev/null @@ -1,11 +0,0 @@ -package mcp - -import ( - "github.com/flipped-aurora/gin-vue-admin/server/service/mcp/client" - "github.com/flipped-aurora/gin-vue-admin/server/service/mcp/service" -) - -type McpGroup struct { - ServiceGroup service.McpServiceGroup - ClientGroup client.ClientStruct -} diff --git a/server/service/mcp/service/enter.go b/server/service/mcp/service/enter.go deleted file mode 100644 index 10b11b77b1..0000000000 --- a/server/service/mcp/service/enter.go +++ /dev/null @@ -1,5 +0,0 @@ -package service - -type McpServiceGroup struct { - McpService -} diff --git a/server/service/mcp/service/service.go b/server/service/mcp/service/service.go deleted file mode 100644 index e63cd92f4c..0000000000 --- a/server/service/mcp/service/service.go +++ /dev/null @@ -1,30 +0,0 @@ -package service - -import ( - "github.com/flipped-aurora/gin-vue-admin/server/initialize/mcp" - model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" -) - -type McpService struct { -} - -var Init = new(mcp.McpServer) - -func (m *McpService) Run() { - - config := &model.McpConfig{ - Url: `127.0.0.1:9999`, - Name: "mcpServer", - Version: "1.0.0", - } - - // 创建mcp服务器-sse方式 - Init.NewSSE(config) - - // 注册工具 - Init.BinTool() - - // 启动mcp服务器 - Init.Run() - -} diff --git a/server/service/mcp/service/tool/enter.go b/server/service/mcp/service/tool/enter.go deleted file mode 100644 index 56d552321d..0000000000 --- a/server/service/mcp/service/tool/enter.go +++ /dev/null @@ -1,6 +0,0 @@ -package tool - -type ToolGroup struct { - ToolList - ToolCall -} diff --git a/server/service/mcp/service/tool/tool_call.go b/server/service/mcp/service/tool/tool_call.go deleted file mode 100644 index 1a18c29157..0000000000 --- a/server/service/mcp/service/tool/tool_call.go +++ /dev/null @@ -1,51 +0,0 @@ -package tool - -import ( - "context" - "encoding/json" - "time" - - "github.com/ThinkInAIXYZ/go-mcp/protocol" - model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" -) - -type ToolCall struct{} - -// 获取当前系统时间 -func (t *ToolCall) CallCurrentTime(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { - // 获取当前系统时间 - currentTime := time.Now().Format("2006-01-02 15:04:05") - //返回 - return &protocol.CallToolResult{ - Content: []protocol.Content{ - protocol.TextContent{ - Type: "text", - Text: currentTime, - }, - }, - }, nil -} - -// 获取天气信息 -// 参数:城市名称 -// 日期: 2023-07-01 - -func (t *ToolCall) CallCityWeather(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { - // 解析请求参数 - req := new(model.WeatherReq) - if err := json.Unmarshal(request.RawArguments, &req); err != nil { - return nil, err - } - - //构造回复信息 - str := "天气信息: " + req.Date + " " + req.City + " 天气晴朗,温度为 25°C,相对湿度为 60%,风速为 10m/s。" - - return &protocol.CallToolResult{ - Content: []protocol.Content{ - protocol.TextContent{ - Type: "text", - Text: str, - }, - }, - }, nil -} diff --git a/server/service/mcp/service/tool/tool_list.go b/server/service/mcp/service/tool/tool_list.go deleted file mode 100644 index 1ba2755228..0000000000 --- a/server/service/mcp/service/tool/tool_list.go +++ /dev/null @@ -1,23 +0,0 @@ -package tool - -import ( - model "github.com/flipped-aurora/gin-vue-admin/server/model/mcp" -) - -type ToolList struct{} - -// 获取当前系统时间 -func (t *ToolList) CurrentTime() (name string, description string, inputReqStruct interface{}) { - name = "CurrentTime" - description = "获取当前系统时间" - inputReqStruct = model.CurrentTimeReq{} - return -} - -// 获取天气 -func (t *ToolList) CityWeather() (name string, description string, inputReqStruct interface{}) { - name = "Weather" - description = "获取天气" - inputReqStruct = model.WeatherReq{} //自动解析参数 - return -} From 4735e1f01feb93df6ab05216b878a52aa19ce01a Mon Sep 17 00:00:00 2001 From: pixelmaxQM Date: Thu, 8 May 2025 22:16:58 +0800 Subject: [PATCH 09/19] =?UTF-8?q?feat:=E8=B0=83=E6=95=B4mcp=E7=BB=93?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E5=92=8C=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/go.mod | 5 +- server/go.sum | 10 ++- server/mcp/client/main.go | 24 ++---- server/mcp/client/main_test.go | 101 +++++++++++++++++++++++++ server/mcp/tool/current_time.go | 39 ++++++++++ server/mcp/tool/current_time/handle.go | 22 ------ server/mcp/tool/current_time/model.go | 7 -- server/mcp/tool/current_time/new.go | 9 --- server/mcp/tool/enter.go | 8 -- server/mcp/tool/get_nickname.go | 54 +++++++++++++ server/mcp/tool/weather/handle.go | 28 ------- server/mcp/tool/weather/model.go | 8 -- server/mcp/tool/weather/new.go | 9 --- 13 files changed, 210 insertions(+), 114 deletions(-) create mode 100644 server/mcp/client/main_test.go create mode 100644 server/mcp/tool/current_time.go delete mode 100644 server/mcp/tool/current_time/handle.go delete mode 100644 server/mcp/tool/current_time/model.go delete mode 100644 server/mcp/tool/current_time/new.go create mode 100644 server/mcp/tool/get_nickname.go delete mode 100644 server/mcp/tool/weather/handle.go delete mode 100644 server/mcp/tool/weather/model.go delete mode 100644 server/mcp/tool/weather/new.go diff --git a/server/go.mod b/server/go.mod index e42a785a53..1d773ba54d 100644 --- a/server/go.mod +++ b/server/go.mod @@ -55,7 +55,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/STARRY-S/zip v0.1.0 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect @@ -114,6 +114,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/microsoft/go-mssqldb v1.8.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minlz v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -121,7 +122,7 @@ require ( github.com/montanaflynn/stats v0.7.1 // indirect github.com/mozillazg/go-httpheader v0.4.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/nwaples/rardecode/v2 v2.0.1 // indirect + github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect github.com/otiai10/mint v1.6.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect diff --git a/server/go.sum b/server/go.sum index 62094ad41b..7dad4da8f3 100644 --- a/server/go.sum +++ b/server/go.sum @@ -46,8 +46,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= -github.com/STARRY-S/zip v0.1.0 h1:eUER3jKmHKXjv+iy3BekLa+QnNSo1Lqz4eTzYBcGDqo= -github.com/STARRY-S/zip v0.1.0/go.mod h1:qj/mTZkvb3AvfGQ2e775/3AODRvB4peSw8KNMvrM8/I= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= github.com/ThinkInAIXYZ/go-mcp v0.2.2 h1:zlm4Xo8pxGzmfTvN16hM0YP75Q7QZ713g3mZSbO3JAs= github.com/ThinkInAIXYZ/go-mcp v0.2.2/go.mod h1:KnUWUymko7rmOgzvIjxwX0uB9oiJeLF/Q3W9cRt8fVg= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= @@ -326,6 +326,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E= github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= +github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= +github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -347,8 +349,8 @@ github.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiY github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nwaples/rardecode/v2 v2.0.1 h1:3MN6/R+Y4c7e+21U3yhWuUcf72sYmcmr6jtiuAVSH1A= -github.com/nwaples/rardecode/v2 v2.0.1/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= diff --git a/server/mcp/client/main.go b/server/mcp/client/main.go index e75c139832..f8b05be276 100644 --- a/server/mcp/client/main.go +++ b/server/mcp/client/main.go @@ -7,25 +7,15 @@ import ( "github.com/ThinkInAIXYZ/go-mcp/transport" ) -type Client struct{} - -var mcpClient *client.Client - -// 链接mcp服务器 -func (c *Client) New() error { - transportClient, err := transport.NewSSEClientTransport("http://localhost:8888/sse") +// 创建MCP客户端 +func NewClient(url string, clientInfo protocol.Implementation) (*client.Client, error) { + transportClient, err := transport.NewSSEClientTransport(url) if err != nil { - return fmt.Errorf("创建mcp客户端失败SSE: %v", err) + return nil, fmt.Errorf("创建MCP客户端失败: %v", err) } - mcpClient, err = client.NewClient(transportClient, client.WithClientInfo(protocol.Implementation{ - Name: "client", - Version: "v0.0.1", - })) + mcpClient, err := client.NewClient(transportClient, client.WithClientInfo(clientInfo)) if err != nil { - return fmt.Errorf("创建mcp客户端失败: %v", err) + return nil, fmt.Errorf("创建MCP客户端失败: %v", err) } - defer mcpClient.Close() - fmt.Println("MCP客户端已启动") - - return nil + return mcpClient, nil } diff --git a/server/mcp/client/main_test.go b/server/mcp/client/main_test.go new file mode 100644 index 0000000000..1fcd410bcc --- /dev/null +++ b/server/mcp/client/main_test.go @@ -0,0 +1,101 @@ +package client + +import ( + "context" + "fmt" + "testing" + + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "github.com/stretchr/testify/assert" +) + +func TestNewClient(t *testing.T) { + t.Run("CurrentTime", func(t *testing.T) { + clientInfo := protocol.Implementation{Name: "TestClient"} + mcpClient, err := NewClient("http://127.0.0.1:8888/sse", clientInfo) + + if err != nil { + t.Error(err) + return + } + + tools, err := mcpClient.ListTools(context.Background()) + if err != nil { + t.Error(err) + return + } + for i := range tools.Tools { + tool := tools.Tools[i] + fmt.Printf("工具名称: %s\n", tool.Name) + fmt.Printf("工具描述: %s\n", tool.Description) + + // 打印参数信息 + if tool.InputSchema.Properties != nil { + fmt.Println("参数列表:") + for paramName, prop := range tool.InputSchema.Properties { + required := "否" + // 检查参数是否在必填列表中 + for _, reqField := range tool.InputSchema.Required { + if reqField == paramName { + required = "是" + break + } + } + fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n", + paramName, prop.Type, prop.Description, required) + } + } else { + fmt.Println("该工具没有参数") + } + fmt.Println("-------------------") + } + + // 测试调用工具 + res, err := mcpClient.CallTool(context.Background(), &protocol.CallToolRequest{ + Name: "CurrentTime", + Arguments: map[string]interface{}{ + "timezone": "UTC", + }, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + for _, content := range res.Content { + if textContent, ok := content.(protocol.TextContent); ok { + fmt.Printf("文本内容: %s\n", textContent.Text) + } else { + fmt.Printf("不是文本: %v\n", content) + } + } + }) + + t.Run("GetNickname", func(t *testing.T) { + clientInfo := protocol.Implementation{Name: "TestClient"} + mcpClient, err := NewClient("http://127.0.0.1:8888/sse", clientInfo) + + if err != nil { + t.Error(err) + return + } + + // 测试调用工具 + res, err := mcpClient.CallTool(context.Background(), &protocol.CallToolRequest{ + Name: "GetNickname", + Arguments: map[string]interface{}{ + "username": "admin", + }, + }) + + if err != nil { + t.Error(err) + return + } + + for _, content := range res.Content { + if textContent, ok := content.(protocol.TextContent); ok { + fmt.Printf("文本内容: %s\n", textContent.Text) + } else { + fmt.Printf("不是文本: %v\n", content) + } + } + }) +} diff --git a/server/mcp/tool/current_time.go b/server/mcp/tool/current_time.go new file mode 100644 index 0000000000..f98c3e9a35 --- /dev/null +++ b/server/mcp/tool/current_time.go @@ -0,0 +1,39 @@ +package tool + +import ( + "context" + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "time" +) + +func init() { + RegisterTool(&CurrentTime{}) +} + +type CurrentTime struct { + Request struct { + Timezone string `json:"timezone" description:"current time timezone"` + } +} + +// 获取当前系统时间 +func (t *CurrentTime) Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { + // 获取当前系统时间 + currentTime := time.Now().Format("2006-01-02 15:04:05") + //返回 + return &protocol.CallToolResult{ + Content: []protocol.Content{ + protocol.TextContent{ + Type: "text", + Text: currentTime, + }, + }, + }, nil +} + +func (t *CurrentTime) New() (name string, description string, inputReqStruct interface{}) { + name = "CurrentTime" + description = "获取当前系统时间" + inputReqStruct = t.Request + return +} diff --git a/server/mcp/tool/current_time/handle.go b/server/mcp/tool/current_time/handle.go deleted file mode 100644 index 9157bc15d0..0000000000 --- a/server/mcp/tool/current_time/handle.go +++ /dev/null @@ -1,22 +0,0 @@ -package current_time - -import ( - "context" - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "time" -) - -// 获取当前系统时间 -func (t *Tool) Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { - // 获取当前系统时间 - currentTime := time.Now().Format("2006-01-02 15:04:05") - //返回 - return &protocol.CallToolResult{ - Content: []protocol.Content{ - protocol.TextContent{ - Type: "text", - Text: currentTime, - }, - }, - }, nil -} diff --git a/server/mcp/tool/current_time/model.go b/server/mcp/tool/current_time/model.go deleted file mode 100644 index bd71c82413..0000000000 --- a/server/mcp/tool/current_time/model.go +++ /dev/null @@ -1,7 +0,0 @@ -package current_time - -type Tool struct{} - -type Req struct { - Timezone string `json:"timezone" description:"current time timezone"` -} diff --git a/server/mcp/tool/current_time/new.go b/server/mcp/tool/current_time/new.go deleted file mode 100644 index 23d78db9df..0000000000 --- a/server/mcp/tool/current_time/new.go +++ /dev/null @@ -1,9 +0,0 @@ -package current_time - -// 获取当前系统时间 -func (t *Tool) New() (name string, description string, inputReqStruct interface{}) { - name = "CurrentTime" - description = "获取当前系统时间" - inputReqStruct = t - return -} diff --git a/server/mcp/tool/enter.go b/server/mcp/tool/enter.go index beb1a0746a..ecfc17d4a7 100644 --- a/server/mcp/tool/enter.go +++ b/server/mcp/tool/enter.go @@ -4,16 +4,8 @@ import ( "context" "github.com/ThinkInAIXYZ/go-mcp/protocol" "github.com/ThinkInAIXYZ/go-mcp/server" - "github.com/flipped-aurora/gin-vue-admin/server/mcp/tool/current_time" - "github.com/flipped-aurora/gin-vue-admin/server/mcp/tool/weather" ) -func init() { - // 注册工具 - RegisterTool(¤t_time.Tool{}) - RegisterTool(&weather.Tool{}) -} - // McpTool 定义了MCP工具必须实现的接口 type McpTool interface { // Handle 返回工具调用信息 diff --git a/server/mcp/tool/get_nickname.go b/server/mcp/tool/get_nickname.go new file mode 100644 index 0000000000..ca16c557ca --- /dev/null +++ b/server/mcp/tool/get_nickname.go @@ -0,0 +1,54 @@ +package tool + +import ( + "context" + "encoding/json" + "fmt" + "github.com/ThinkInAIXYZ/go-mcp/protocol" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +func init() { + RegisterTool(&GetNickname{}) +} + +type GetNickname struct { + Request struct { + Username string `json:"username" description:"用户的username"` + } +} + +// 获取天气 +func (t *GetNickname) New() (name string, description string, inputReqStruct interface{}) { + name = "GetNickname" + description = "根据用户username获取nickname" + inputReqStruct = t.Request //自动解析参数 + return +} + +// 获取天气 +func (t *GetNickname) Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { + // 解析请求参数 + if err := json.Unmarshal(request.RawArguments, &t.Request); err != nil { + return nil, err + } + + var user system.SysUser + + err := global.GVA_DB.First(&user, "username = ?", t.Request.Username).Error + + if err != nil { + return nil, err + } + + //构造回复信息 + return &protocol.CallToolResult{ + Content: []protocol.Content{ + protocol.TextContent{ + Type: "text", + Text: fmt.Sprintf("%s的昵称是%s", t.Request.Username, user.NickName), + }, + }, + }, nil +} diff --git a/server/mcp/tool/weather/handle.go b/server/mcp/tool/weather/handle.go deleted file mode 100644 index c10d285c2f..0000000000 --- a/server/mcp/tool/weather/handle.go +++ /dev/null @@ -1,28 +0,0 @@ -package weather - -import ( - "context" - "encoding/json" - "github.com/ThinkInAIXYZ/go-mcp/protocol" -) - -// 获取天气 -func (t *Tool) Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { - var req Request - // 解析请求参数 - if err := json.Unmarshal(request.RawArguments, &req); err != nil { - return nil, err - } - - //构造回复信息 - str := "天气信息: " + req.Date + " " + req.City + " 天气晴朗,温度为 25°C,相对湿度为 60%,风速为 10m/s。" - - return &protocol.CallToolResult{ - Content: []protocol.Content{ - protocol.TextContent{ - Type: "text", - Text: str, - }, - }, - }, nil -} diff --git a/server/mcp/tool/weather/model.go b/server/mcp/tool/weather/model.go deleted file mode 100644 index f06438bbe9..0000000000 --- a/server/mcp/tool/weather/model.go +++ /dev/null @@ -1,8 +0,0 @@ -package weather - -type Tool struct{} - -type Request struct { - City string `json:"city" description:"city name"` - Date string `json:"date" description:"date"` -} diff --git a/server/mcp/tool/weather/new.go b/server/mcp/tool/weather/new.go deleted file mode 100644 index 5211a187d5..0000000000 --- a/server/mcp/tool/weather/new.go +++ /dev/null @@ -1,9 +0,0 @@ -package weather - -// 获取天气 -func (t *Tool) New() (name string, description string, inputReqStruct interface{}) { - name = "Weather" - description = "获取天气" - inputReqStruct = t //自动解析参数 - return -} From a0047882c0fe4ff2939faa4f8901fc4ffd71c4bd Mon Sep 17 00:00:00 2001 From: pixelmaxQM Date: Fri, 9 May 2025 00:06:02 +0800 Subject: [PATCH 10/19] =?UTF-8?q?feat:=E6=9B=B4=E6=8D=A2mcp=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E5=8C=85=E5=92=8C=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/core/server.go | 4 +- server/core/server_run.go | 11 +--- server/go.mod | 5 +- server/go.sum | 2 + server/initialize/mcp.go | 23 ++++++++ server/initialize/router.go | 16 ++--- server/main.go | 1 + server/mcp/client/client.go | 39 ++++++++++++ server/mcp/client/main.go | 21 ------- server/mcp/client/main_test.go | 101 -------------------------------- server/mcp/current_time.go | 38 ++++++++++++ server/mcp/enter.go | 31 ++++++++++ server/mcp/get_nickname.go | 52 ++++++++++++++++ server/mcp/main.go | 39 ------------ server/mcp/tool/current_time.go | 39 ------------ server/mcp/tool/enter.go | 35 ----------- server/mcp/tool/get_nickname.go | 54 ----------------- 17 files changed, 198 insertions(+), 313 deletions(-) create mode 100644 server/initialize/mcp.go create mode 100644 server/mcp/client/client.go delete mode 100644 server/mcp/client/main.go delete mode 100644 server/mcp/client/main_test.go create mode 100644 server/mcp/current_time.go create mode 100644 server/mcp/enter.go create mode 100644 server/mcp/get_nickname.go delete mode 100644 server/mcp/main.go delete mode 100644 server/mcp/tool/current_time.go delete mode 100644 server/mcp/tool/enter.go delete mode 100644 server/mcp/tool/get_nickname.go diff --git a/server/core/server.go b/server/core/server.go index 68eb927db1..af79dd9045 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -29,7 +29,7 @@ func RunServer() { system.LoadAll() } - Router, mcpServer := initialize.Routers() + Router := initialize.Routers() address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr) @@ -49,5 +49,5 @@ func RunServer() { ** 版权持有公司:北京翻转极光科技有限责任公司 ** ** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html ** `, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath) - initServer(address, Router, mcpServer, 10*time.Minute, 10*time.Minute) + initServer(address, Router, 10*time.Minute, 10*time.Minute) } diff --git a/server/core/server_run.go b/server/core/server_run.go index 14e8a43a5f..067ce6b6e6 100644 --- a/server/core/server_run.go +++ b/server/core/server_run.go @@ -3,7 +3,6 @@ package core import ( "context" "fmt" - "github.com/ThinkInAIXYZ/go-mcp/transport" "net/http" "os" "os/signal" @@ -20,7 +19,7 @@ type server interface { } // initServer 启动服务并实现优雅关闭 -func initServer(address string, router *gin.Engine, mcpServer transport.ServerTransport, readTimeout, writeTimeout time.Duration) { +func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { // 创建服务 srv := &http.Server{ Addr: address, @@ -58,12 +57,4 @@ func initServer(address string, router *gin.Engine, mcpServer transport.ServerTr } zap.L().Info("WEB服务已关闭") - - zap.L().Info("关闭MCP服务...") - - if err := mcpServer.Shutdown(ctx, ctx); err != nil { - zap.L().Fatal("MCP服务器关闭异常", zap.Error(err)) - } - - zap.L().Info("MCP服务器已关闭") } diff --git a/server/go.mod b/server/go.mod index 1d773ba54d..b70c6f5661 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,6 +1,8 @@ module github.com/flipped-aurora/gin-vue-admin/server -go 1.22.2 +go 1.23 + +toolchain go1.23.9 require ( github.com/ThinkInAIXYZ/go-mcp v0.2.2 @@ -111,6 +113,7 @@ require ( github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/magiconair/properties v1.8.9 // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/mark3labs/mcp-go v0.26.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/microsoft/go-mssqldb v1.8.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect diff --git a/server/go.sum b/server/go.sum index 7dad4da8f3..dba8883f00 100644 --- a/server/go.sum +++ b/server/go.sum @@ -312,6 +312,8 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.26.0 h1:xz/Kv1cHLYovF8txv6btBM39/88q3YOjnxqhi51jB0w= +github.com/mark3labs/mcp-go v0.26.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= diff --git a/server/initialize/mcp.go b/server/initialize/mcp.go new file mode 100644 index 0000000000..fdcb65f6e2 --- /dev/null +++ b/server/initialize/mcp.go @@ -0,0 +1,23 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func McpRun() *server.SSEServer { + config := global.GVA_CONFIG.MCP + + s := server.NewMCPServer( + config.Name, + config.Version, + ) + + mcpTool.RegisterAllTools(s) + + return server.NewSSEServer(s, + server.WithSSEEndpoint(config.SSEPath), + server.WithMessageEndpoint(config.MessagePath), + server.WithBaseURL(config.UrlPrefix)) +} diff --git a/server/initialize/router.go b/server/initialize/router.go index cb9beee669..4e6fd083e4 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -1,9 +1,6 @@ package initialize import ( - "github.com/ThinkInAIXYZ/go-mcp/transport" - "github.com/flipped-aurora/gin-vue-admin/server/mcp" - "go.uber.org/zap" "net/http" "os" @@ -36,25 +33,22 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) { // 初始化总路由 -func Routers() (*gin.Engine, transport.ServerTransport) { +func Routers() *gin.Engine { Router := gin.New() Router.Use(gin.Recovery()) if gin.Mode() == gin.DebugMode { Router.Use(gin.Logger()) } - mcpServer, mcpHandler, err := mcp.Run() - if err != nil { - global.GVA_LOG.Error("创建MCP服务器失败", zap.Error(err)) - } + sseServer := McpRun() // 注册mcp服务 Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) { - mcpHandler.HandleSSE().ServeHTTP(c.Writer, c.Request) + sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request) }) Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) { - mcpHandler.HandleMessage().ServeHTTP(c.Writer, c.Request) + sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request) }) systemRouter := router.RouterGroupApp.System @@ -124,5 +118,5 @@ func Routers() (*gin.Engine, transport.ServerTransport) { global.GVA_ROUTERS = Router.Routes() global.GVA_LOG.Info("router register success") - return Router, mcpServer + return Router } diff --git a/server/main.go b/server/main.go index a079ca94ae..f4b5be45a5 100644 --- a/server/main.go +++ b/server/main.go @@ -45,6 +45,7 @@ func initializeSystem() { initialize.Timer() initialize.DBList() initialize.SetupHandlers() // 注册全局函数 + initialize.McpRun() if global.GVA_DB != nil { initialize.RegisterTables() // 初始化表 } diff --git a/server/mcp/client/client.go b/server/mcp/client/client.go new file mode 100644 index 0000000000..7e5db1ef6d --- /dev/null +++ b/server/mcp/client/client.go @@ -0,0 +1,39 @@ +package client + +import ( + "context" + "errors" + mcpClient "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +func NewClient(baseUrl, name, version, serverName string) (*mcpClient.Client, error) { + client, err := mcpClient.NewSSEMCPClient(baseUrl) + if err != nil { + return nil, err + } + + ctx := context.Background() + + // 启动client + if err := client.Start(ctx); err != nil { + return nil, err + } + + // 初始化 + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: name, + Version: version, + } + + result, err := client.Initialize(ctx, initRequest) + if err != nil { + return nil, err + } + if result.ServerInfo.Name != serverName { + return nil, errors.New("server name mismatch") + } + return client, nil +} diff --git a/server/mcp/client/main.go b/server/mcp/client/main.go deleted file mode 100644 index f8b05be276..0000000000 --- a/server/mcp/client/main.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "fmt" - "github.com/ThinkInAIXYZ/go-mcp/client" - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "github.com/ThinkInAIXYZ/go-mcp/transport" -) - -// 创建MCP客户端 -func NewClient(url string, clientInfo protocol.Implementation) (*client.Client, error) { - transportClient, err := transport.NewSSEClientTransport(url) - if err != nil { - return nil, fmt.Errorf("创建MCP客户端失败: %v", err) - } - mcpClient, err := client.NewClient(transportClient, client.WithClientInfo(clientInfo)) - if err != nil { - return nil, fmt.Errorf("创建MCP客户端失败: %v", err) - } - return mcpClient, nil -} diff --git a/server/mcp/client/main_test.go b/server/mcp/client/main_test.go deleted file mode 100644 index 1fcd410bcc..0000000000 --- a/server/mcp/client/main_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package client - -import ( - "context" - "fmt" - "testing" - - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "github.com/stretchr/testify/assert" -) - -func TestNewClient(t *testing.T) { - t.Run("CurrentTime", func(t *testing.T) { - clientInfo := protocol.Implementation{Name: "TestClient"} - mcpClient, err := NewClient("http://127.0.0.1:8888/sse", clientInfo) - - if err != nil { - t.Error(err) - return - } - - tools, err := mcpClient.ListTools(context.Background()) - if err != nil { - t.Error(err) - return - } - for i := range tools.Tools { - tool := tools.Tools[i] - fmt.Printf("工具名称: %s\n", tool.Name) - fmt.Printf("工具描述: %s\n", tool.Description) - - // 打印参数信息 - if tool.InputSchema.Properties != nil { - fmt.Println("参数列表:") - for paramName, prop := range tool.InputSchema.Properties { - required := "否" - // 检查参数是否在必填列表中 - for _, reqField := range tool.InputSchema.Required { - if reqField == paramName { - required = "是" - break - } - } - fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n", - paramName, prop.Type, prop.Description, required) - } - } else { - fmt.Println("该工具没有参数") - } - fmt.Println("-------------------") - } - - // 测试调用工具 - res, err := mcpClient.CallTool(context.Background(), &protocol.CallToolRequest{ - Name: "CurrentTime", - Arguments: map[string]interface{}{ - "timezone": "UTC", - }, - }) - assert.NoError(t, err) - assert.NotNil(t, res) - for _, content := range res.Content { - if textContent, ok := content.(protocol.TextContent); ok { - fmt.Printf("文本内容: %s\n", textContent.Text) - } else { - fmt.Printf("不是文本: %v\n", content) - } - } - }) - - t.Run("GetNickname", func(t *testing.T) { - clientInfo := protocol.Implementation{Name: "TestClient"} - mcpClient, err := NewClient("http://127.0.0.1:8888/sse", clientInfo) - - if err != nil { - t.Error(err) - return - } - - // 测试调用工具 - res, err := mcpClient.CallTool(context.Background(), &protocol.CallToolRequest{ - Name: "GetNickname", - Arguments: map[string]interface{}{ - "username": "admin", - }, - }) - - if err != nil { - t.Error(err) - return - } - - for _, content := range res.Content { - if textContent, ok := content.(protocol.TextContent); ok { - fmt.Printf("文本内容: %s\n", textContent.Text) - } else { - fmt.Printf("不是文本: %v\n", content) - } - } - }) -} diff --git a/server/mcp/current_time.go b/server/mcp/current_time.go new file mode 100644 index 0000000000..43a6277065 --- /dev/null +++ b/server/mcp/current_time.go @@ -0,0 +1,38 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" + "time" +) + +func init() { + RegisterTool(&CurrentTime{}) +} + +type CurrentTime struct { +} + +// 获取当前系统时间 +func (t *CurrentTime) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 获取当前系统时间 + currentTime := time.Now().Format("2006-01-02 15:04:05") + //返回 + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: currentTime, + }, + }, + }, nil +} + +func (t *CurrentTime) New() mcp.Tool { + return mcp.NewTool("currentTime", + mcp.WithDescription("获取当前系统时间"), + mcp.WithString("timezone", + mcp.Required(), + mcp.Description("时区"), + )) +} diff --git a/server/mcp/enter.go b/server/mcp/enter.go new file mode 100644 index 0000000000..ca19f54c0e --- /dev/null +++ b/server/mcp/enter.go @@ -0,0 +1,31 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// McpTool 定义了MCP工具必须实现的接口 +type McpTool interface { + // Handle 返回工具调用信息 + Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + // New 返回工具注册信息 + New() mcp.Tool +} + +// 工具注册表 +var toolRegister = make(map[string]McpTool) + +// RegisterTool 供工具在init时调用,将自己注册到工具注册表中 +func RegisterTool(tool McpTool) { + mcpTool := tool.New() + toolRegister[mcpTool.Name] = tool +} + +// RegisterAllTools 将所有注册的工具注册到MCP服务中 +func RegisterAllTools(mcpServer *server.MCPServer) { + for _, tool := range toolRegister { + mcpServer.AddTool(tool.New(), tool.Handle) + } +} diff --git a/server/mcp/get_nickname.go b/server/mcp/get_nickname.go new file mode 100644 index 0000000000..fedbc4ba09 --- /dev/null +++ b/server/mcp/get_nickname.go @@ -0,0 +1,52 @@ +package mcpTool + +import ( + "context" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&GetNickname{}) +} + +type GetNickname struct{} + +// 根据用户username获取nickname +func (t *GetNickname) New() mcp.Tool { + return mcp.NewTool("getNickname", + mcp.WithDescription("根据用户username获取nickname"), + mcp.WithString("username", + mcp.Required(), + mcp.Description("用户的username"), + )) +} + +// 根据用户username获取nickname +func (t *GetNickname) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + name, ok := request.Params.Arguments["username"].(string) + if !ok { + return nil, errors.New("name must be a string") + } + + var user system.SysUser + + err := global.GVA_DB.First(&user, "username = ?", name).Error + + if err != nil { + return nil, err + } + + //构造回复信息 + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("%s的昵称是%s", name, user.NickName), + }, + }, + }, nil +} diff --git a/server/mcp/main.go b/server/mcp/main.go deleted file mode 100644 index b745889470..0000000000 --- a/server/mcp/main.go +++ /dev/null @@ -1,39 +0,0 @@ -package mcp - -import ( - "fmt" - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "github.com/ThinkInAIXYZ/go-mcp/server" - "github.com/ThinkInAIXYZ/go-mcp/transport" - "github.com/flipped-aurora/gin-vue-admin/server/global" - "github.com/flipped-aurora/gin-vue-admin/server/mcp/tool" -) - -func Run() (transport.ServerTransport, *transport.SSEHandler, error) { - // 初始化SSE服务器 - config := global.GVA_CONFIG.MCP - transportServer, mcpHandle, err := transport.NewSSEServerTransportAndHandler(config.MessagePath, - transport.WithSSEServerTransportAndHandlerOptionLogger(global.GVA_LOG.Sugar()), - ) - if err != nil { - return nil, nil, fmt.Errorf("创建SSE服务器失败: %v", err) - } - mcpServer, err := server.NewServer(transportServer, - server.WithServerInfo(protocol.Implementation{ - Name: config.Name, - Version: config.Version, - }), - ) - if err != nil { - return nil, nil, fmt.Errorf("创建MCP服务器失败: %v", err) - } - - tool.RegisterAllTools(mcpServer) - - //启动服务器 - go func() { - err = mcpServer.Run() - }() - - return transportServer, mcpHandle, err -} diff --git a/server/mcp/tool/current_time.go b/server/mcp/tool/current_time.go deleted file mode 100644 index f98c3e9a35..0000000000 --- a/server/mcp/tool/current_time.go +++ /dev/null @@ -1,39 +0,0 @@ -package tool - -import ( - "context" - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "time" -) - -func init() { - RegisterTool(&CurrentTime{}) -} - -type CurrentTime struct { - Request struct { - Timezone string `json:"timezone" description:"current time timezone"` - } -} - -// 获取当前系统时间 -func (t *CurrentTime) Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { - // 获取当前系统时间 - currentTime := time.Now().Format("2006-01-02 15:04:05") - //返回 - return &protocol.CallToolResult{ - Content: []protocol.Content{ - protocol.TextContent{ - Type: "text", - Text: currentTime, - }, - }, - }, nil -} - -func (t *CurrentTime) New() (name string, description string, inputReqStruct interface{}) { - name = "CurrentTime" - description = "获取当前系统时间" - inputReqStruct = t.Request - return -} diff --git a/server/mcp/tool/enter.go b/server/mcp/tool/enter.go deleted file mode 100644 index ecfc17d4a7..0000000000 --- a/server/mcp/tool/enter.go +++ /dev/null @@ -1,35 +0,0 @@ -package tool - -import ( - "context" - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "github.com/ThinkInAIXYZ/go-mcp/server" -) - -// McpTool 定义了MCP工具必须实现的接口 -type McpTool interface { - // Handle 返回工具调用信息 - Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) - // New 返回工具注册信息 - New() (name string, description string, inputReqStruct interface{}) -} - -// 工具注册表 -var toolRegister = make(map[string]McpTool) - -// RegisterTool 供工具在init时调用,将自己注册到工具注册表中 -func RegisterTool(tool McpTool) { - name, _, _ := tool.New() - toolRegister[name] = tool -} - -// RegisterAllTools 将所有注册的工具注册到MCP服务中 -func RegisterAllTools(mcpServer *server.Server) { - for _, tool := range toolRegister { - t, e := protocol.NewTool(tool.New()) - if e != nil { - panic("tool register error: " + e.Error()) - } - mcpServer.RegisterTool(t, tool.Handle) - } -} diff --git a/server/mcp/tool/get_nickname.go b/server/mcp/tool/get_nickname.go deleted file mode 100644 index ca16c557ca..0000000000 --- a/server/mcp/tool/get_nickname.go +++ /dev/null @@ -1,54 +0,0 @@ -package tool - -import ( - "context" - "encoding/json" - "fmt" - "github.com/ThinkInAIXYZ/go-mcp/protocol" - "github.com/flipped-aurora/gin-vue-admin/server/global" - "github.com/flipped-aurora/gin-vue-admin/server/model/system" -) - -func init() { - RegisterTool(&GetNickname{}) -} - -type GetNickname struct { - Request struct { - Username string `json:"username" description:"用户的username"` - } -} - -// 获取天气 -func (t *GetNickname) New() (name string, description string, inputReqStruct interface{}) { - name = "GetNickname" - description = "根据用户username获取nickname" - inputReqStruct = t.Request //自动解析参数 - return -} - -// 获取天气 -func (t *GetNickname) Handle(ctx context.Context, request *protocol.CallToolRequest) (*protocol.CallToolResult, error) { - // 解析请求参数 - if err := json.Unmarshal(request.RawArguments, &t.Request); err != nil { - return nil, err - } - - var user system.SysUser - - err := global.GVA_DB.First(&user, "username = ?", t.Request.Username).Error - - if err != nil { - return nil, err - } - - //构造回复信息 - return &protocol.CallToolResult{ - Content: []protocol.Content{ - protocol.TextContent{ - Type: "text", - Text: fmt.Sprintf("%s的昵称是%s", t.Request.Username, user.NickName), - }, - }, - }, nil -} From 3970b888cb51cc34a8fd76f34abfdab83b742918 Mon Sep 17 00:00:00 2001 From: pixelmaxQM Date: Fri, 9 May 2025 00:18:36 +0800 Subject: [PATCH 11/19] =?UTF-8?q?feat:=E6=8F=90=E4=BA=A4=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E5=B7=A5=E5=85=B7=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/mcp/client/client_test.go | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 server/mcp/client/client_test.go diff --git a/server/mcp/client/client_test.go b/server/mcp/client/client_test.go new file mode 100644 index 0000000000..917d22d397 --- /dev/null +++ b/server/mcp/client/client_test.go @@ -0,0 +1,132 @@ +package client + +import ( + "context" + "fmt" + "github.com/mark3labs/mcp-go/mcp" + "testing" +) + +// 测试 MCP 客户端连接 +func TestMcpClientConnection(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf(err.Error()) + } +} + +func TestTools(t *testing.T) { + t.Run("currentTime", func(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + request := mcp.CallToolRequest{} + request.Params.Name = "currentTime" + request.Params.Arguments = map[string]interface{}{ + "timezone": "UTC+8", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) + + t.Run("getNickname", func(t *testing.T) { + + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + // Initialize + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "test-client", + Version: "1.0.0", + } + + _, err = c.Initialize(ctx, initRequest) + if err != nil { + t.Fatalf("初始化失败: %v", err) + } + + request := mcp.CallToolRequest{} + request.Params.Name = "getNickname" + request.Params.Arguments = map[string]interface{}{ + "username": "admin", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) +} + +func TestGetTools(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + toolsRequest := mcp.ListToolsRequest{} + + toolListResult, err := c.ListTools(ctx, toolsRequest) + if err != nil { + t.Fatalf("获取工具列表失败: %v", err) + } + for i := range toolListResult.Tools { + tool := toolListResult.Tools[i] + fmt.Printf("工具名称: %s\n", tool.Name) + fmt.Printf("工具描述: %s\n", tool.Description) + + // 打印参数信息 + if tool.InputSchema.Properties != nil { + fmt.Println("参数列表:") + for paramName, prop := range tool.InputSchema.Properties { + required := "否" + // 检查参数是否在必填列表中 + for _, reqField := range tool.InputSchema.Required { + if reqField == paramName { + required = "是" + break + } + } + fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n", + paramName, prop.(map[string]any)["type"], prop.(map[string]any)["description"], required) + } + } else { + fmt.Println("该工具没有参数") + } + fmt.Println("-------------------") + } +} From b6c8c5060f7f1d1e4dadbed77fbaf4c63530f33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Fri, 9 May 2025 14:09:32 +0800 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=88=9B=E5=BB=BA=20mcp=20Tool=E6=A8=A1=E6=9D=BF=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/v1/system/auto_code_mcp.go | 34 ++++ server/mcp/current_time.go | 1 + .../model/system/request/sys_auto_code_mcp.go | 15 ++ server/router/system/sys_auto_code.go | 3 + server/service/system/auto_code_mcp.go | 45 ++++++ server/utils/autocode/template_funcs.go | 1 + server/utils/fmt_plus.go | 17 ++ web/src/api/autoCode.js | 8 + web/src/pinia/modules/router.js | 2 - web/src/view/systemTools/autoCode/mcp.vue | 145 ++++++++++++++++++ 10 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 server/api/v1/system/auto_code_mcp.go create mode 100644 server/model/system/request/sys_auto_code_mcp.go create mode 100644 server/service/system/auto_code_mcp.go create mode 100644 web/src/view/systemTools/autoCode/mcp.vue diff --git a/server/api/v1/system/auto_code_mcp.go b/server/api/v1/system/auto_code_mcp.go new file mode 100644 index 0000000000..79393cba7b --- /dev/null +++ b/server/api/v1/system/auto_code_mcp.go @@ -0,0 +1,34 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" +) + +// Create +// @Tags mcp +// @Summary 自动McpTool +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoMcpTool true "创建自动代码" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/mcp [post] +func (a *AutoCodeTemplateApi) MCP(c *gin.Context) { + var info request.AutoMcpTool + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + err = autoCodeTemplateService.CreateMcp(c.Request.Context(), info) + if err != nil { + response.FailWithMessage("创建失败", c) + global.GVA_LOG.Error(err.Error()) + return + } + response.OkWithMessage("创建成功", c) +} diff --git a/server/mcp/current_time.go b/server/mcp/current_time.go index 43a6277065..d6f0fc7a8a 100644 --- a/server/mcp/current_time.go +++ b/server/mcp/current_time.go @@ -34,5 +34,6 @@ func (t *CurrentTime) New() mcp.Tool { mcp.WithString("timezone", mcp.Required(), mcp.Description("时区"), + mcp.Enum("UTC", "CST", "PST", "EST", "GMT", "CET", "JST", "MST", "IST", "AST", "HST"), )) } diff --git a/server/model/system/request/sys_auto_code_mcp.go b/server/model/system/request/sys_auto_code_mcp.go new file mode 100644 index 0000000000..5de7d1d674 --- /dev/null +++ b/server/model/system/request/sys_auto_code_mcp.go @@ -0,0 +1,15 @@ +package request + +type AutoMcpTool struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Params []struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Type string `json:"type" form:"type" binding:"required"` // string, number, boolean, object, array + Required bool `json:"required" form:"required"` + } `json:"params" form:"params"` + Response []struct { + Type string `json:"type" form:"type" binding:"required"` // text, image + } `json:"response" form:"response"` +} diff --git a/server/router/system/sys_auto_code.go b/server/router/system/sys_auto_code.go index e25e1cef8e..68fb8494e2 100644 --- a/server/router/system/sys_auto_code.go +++ b/server/router/system/sys_auto_code.go @@ -19,6 +19,9 @@ func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPubli autoCodeRouter.POST("createTemp", autoCodeTemplateApi.Create) // 创建自动化代码 autoCodeRouter.POST("addFunc", autoCodeTemplateApi.AddFunc) // 为代码插入方法 } + { + autoCodeRouter.POST("mcp", autoCodeTemplateApi.MCP) // 自动创建Mcp Tool模板 + } { autoCodeRouter.POST("getPackage", autoCodePackageApi.All) // 获取package包 autoCodeRouter.POST("delPackage", autoCodePackageApi.Delete) // 删除package包 diff --git a/server/service/system/auto_code_mcp.go b/server/service/system/auto_code_mcp.go new file mode 100644 index 0000000000..291f24fbbb --- /dev/null +++ b/server/service/system/auto_code_mcp.go @@ -0,0 +1,45 @@ +package system + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/flipped-aurora/gin-vue-admin/server/utils/autocode" + "os" + "path/filepath" + "text/template" +) + +func (s *autoCodeTemplate) CreateMcp(ctx context.Context, info request.AutoMcpTool) (err error) { + mcpTemplatePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "mcp", "tools.tpl") + mcpToolPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "mcp") + + var files *template.Template + + templateName := filepath.Base(mcpTemplatePath) + + files, err = template.New(templateName).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(mcpTemplatePath) + if err != nil { + return err + } + + fileName := utils.HumpToUnderscore(info.Name) + + toolFilePath := filepath.Join(mcpToolPath, fileName+".go") + + f, err := os.Create(toolFilePath) + if err != nil { + return err + } + defer f.Close() + + // 执行模板,将内容写入文件 + err = files.Execute(f, info) + if err != nil { + return err + } + + return + +} diff --git a/server/utils/autocode/template_funcs.go b/server/utils/autocode/template_funcs.go index fd03e20108..3aab00332b 100644 --- a/server/utils/autocode/template_funcs.go +++ b/server/utils/autocode/template_funcs.go @@ -11,6 +11,7 @@ import ( // GetTemplateFuncMap 返回模板函数映射,用于在模板中使用 func GetTemplateFuncMap() template.FuncMap { return template.FuncMap{ + "title": strings.Title, "GenerateField": GenerateField, "GenerateSearchField": GenerateSearchField, "GenerateSearchConditions": GenerateSearchConditions, diff --git a/server/utils/fmt_plus.go b/server/utils/fmt_plus.go index 6f34cb47c4..1257a65995 100644 --- a/server/utils/fmt_plus.go +++ b/server/utils/fmt_plus.go @@ -68,6 +68,23 @@ func MaheHump(s string) string { return strings.Join(words, "") } +// HumpToUnderscore 将驼峰命名转换为下划线分割模式 +func HumpToUnderscore(s string) string { + var result strings.Builder + + for i, char := range s { + if i > 0 && char >= 'A' && char <= 'Z' { + // 在大写字母前添加下划线 + result.WriteRune('_') + result.WriteRune(char - 'A' + 'a') // 转小写 + } else { + result.WriteRune(char) + } + } + + return strings.ToLower(result.String()) +} + // RandomString 随机字符串 func RandomString(n int) string { var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") diff --git a/web/src/api/autoCode.js b/web/src/api/autoCode.js index 7fe1af98cb..a6a1118bd5 100644 --- a/web/src/api/autoCode.js +++ b/web/src/api/autoCode.js @@ -207,3 +207,11 @@ export const initAPI = (data) => { data }) } + +export const mcp = (data) => { + return service({ + url: '/autoCode/mcp', + method: 'post', + data + }) +} diff --git a/web/src/pinia/modules/router.js b/web/src/pinia/modules/router.js index 8bd30681e5..36af51322f 100644 --- a/web/src/pinia/modules/router.js +++ b/web/src/pinia/modules/router.js @@ -103,8 +103,6 @@ export const useRouterStore = defineStore('router', () => { // 初始化菜单内容,防止重复添加 topMenu.value = []; asyncRouters.value[0]?.children.forEach((item) => { - // 初始化菜单内容,防止重复添加 - topMenu.value = []; if (item.hidden) return menuMap[item.name] = item topMenu.value.push({ ...item, children: [] }) diff --git a/web/src/view/systemTools/autoCode/mcp.vue b/web/src/view/systemTools/autoCode/mcp.vue new file mode 100644 index 0000000000..93d21a690a --- /dev/null +++ b/web/src/view/systemTools/autoCode/mcp.vue @@ -0,0 +1,145 @@ + + + From 9df778fec7a7b89e1b073ee7eac69cf7a1d7ec77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?piexlMax=28=E5=A5=87=E6=B7=BC?= Date: Fri, 9 May 2025 14:21:56 +0800 Subject: [PATCH 13/19] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/system/request/sys_auto_code_mcp.go | 1 + server/resource/mcp/tools.tpl | 57 +++++++++++++++++++ web/src/view/systemTools/autoCode/mcp.vue | 5 ++ 3 files changed, 63 insertions(+) create mode 100644 server/resource/mcp/tools.tpl diff --git a/server/model/system/request/sys_auto_code_mcp.go b/server/model/system/request/sys_auto_code_mcp.go index 5de7d1d674..a52ec7c500 100644 --- a/server/model/system/request/sys_auto_code_mcp.go +++ b/server/model/system/request/sys_auto_code_mcp.go @@ -8,6 +8,7 @@ type AutoMcpTool struct { Description string `json:"description" form:"description" binding:"required"` Type string `json:"type" form:"type" binding:"required"` // string, number, boolean, object, array Required bool `json:"required" form:"required"` + Default string `json:"default" form:"default"` } `json:"params" form:"params"` Response []struct { Type string `json:"type" form:"type" binding:"required"` // text, image diff --git a/server/resource/mcp/tools.tpl b/server/resource/mcp/tools.tpl new file mode 100644 index 0000000000..c01790cfe8 --- /dev/null +++ b/server/resource/mcp/tools.tpl @@ -0,0 +1,57 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" + "time" +) + +func init() { + RegisterTool(&{{.Name | title}}{}) +} + +type {{.Name | title}} struct { +} + +// {{.Description}} +func (t *{{.Name | title}}) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // TODO: 实现工具逻辑 + // 参数示例: + // {{- range .Params}} + // {{.Name}} := request.Params["{{.Name}}"] + // {{- end}} + return &mcp.CallToolResult{ + Content: []mcp.Content{ + {{- range .Response}} + mcp.{{.Type | title}}Content{ + Type: "{{.Type}}", + // TODO: 填充{{.Type}}内容 + }, + {{- end}} + }, + }, nil +} + +func (t *{{.Name | title}}) New() mcp.Tool { + return mcp.NewTool("{{.Name}}", + mcp.WithDescription("{{.Description}}"), + {{- range .Params}} + mcp.With{{.Type | title}}("{{.Name}}", + {{- if .Required}}mcp.Required(),{{end}} + mcp.Description("{{.Description}}"), + {{- if .Default}} + {{- if eq .Type "string"}} + mcp.DefaultString("{{.Default}}"), + {{- else if eq .Type "number"}} + mcp.DefaultNumber({{.Default}}), + {{- else if eq .Type "boolean"}} + mcp.DefaultBoolean({{if or (eq .Default "true") (eq .Default "True")}}true{{else}}false{{end}}), + {{- else if eq .Type "array"}} + // 注意:数组默认值需要在后端代码中预处理为正确的格式 + // mcp.DefaultArray({{.Default}}), + {{- end}} + {{- end}} + ), + {{- end}} + ) +} diff --git a/web/src/view/systemTools/autoCode/mcp.vue b/web/src/view/systemTools/autoCode/mcp.vue index 93d21a690a..002d2eb8cd 100644 --- a/web/src/view/systemTools/autoCode/mcp.vue +++ b/web/src/view/systemTools/autoCode/mcp.vue @@ -30,6 +30,11 @@ + + +