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..14dc315719 --- /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 + } + + toolFilePath, err := autoCodeTemplateService.CreateMcp(c.Request.Context(), info) + if err != nil { + response.FailWithMessage("创建失败", c) + global.GVA_LOG.Error(err.Error()) + return + } + response.OkWithMessage("创建成功,MCP Tool路径:"+toolFilePath, c) +} 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 aa41c2f469..6dabb0abed 100644 --- a/server/api/v1/system/sys_system.go +++ b/server/api/v1/system/sys_system.go @@ -55,19 +55,20 @@ 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() + // 触发系统重载事件 + err := utils.GlobalSystemEvents.TriggerReload() 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/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/config.yaml b/server/config.yaml index c3ef828809..758d6dcb2f 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: GVA_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 64c5ff04e5..3867a15ab0 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -6,13 +6,10 @@ 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" ) -type server interface { - ListenAndServe() error -} - -func RunWindowsServer() { +func RunServer() { if global.GVA_CONFIG.System.UseRedis { // 初始化redis服务 initialize.Redis() @@ -35,23 +32,22 @@ 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 - 当前版本:v2.8.1 + 当前版本:v2.8.2 加群方式:微信号:shouzi_1994 QQ群:470239250 项目地址:https://github.com/flipped-aurora/gin-vue-admin 插件市场: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) - global.GVA_LOG.Error(s.ListenAndServe().Error()) +`, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath) + initServer(address, Router, 10*time.Minute, 10*time.Minute) } 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..067ce6b6e6 --- /dev/null +++ b/server/core/server_run.go @@ -0,0 +1,60 @@ +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 +} + +// initServer 启动服务并实现优雅关闭 +func initServer(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("关闭WEB服务...") + + // 设置5秒的超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + zap.L().Fatal("WEB服务关闭异常", zap.Error(err)) + } + + zap.L().Info("WEB服务已关闭") +} 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/docs/docs.go b/server/docs/docs.go index f553400a5e..6f0eb2a0da 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -9296,7 +9296,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "v2.8.1", + Version: "v2.8.2", Host: "", BasePath: "", Schemes: []string{}, diff --git a/server/global/global.go b/server/global/global.go index 7291d2a307..97b57c4a6b 100644 --- a/server/global/global.go +++ b/server/global/global.go @@ -2,6 +2,7 @@ package global import ( "fmt" + "github.com/mark3labs/mcp-go/server" "sync" "github.com/gin-gonic/gin" @@ -35,6 +36,7 @@ var ( GVA_Concurrency_Control = &singleflight.Group{} GVA_ROUTERS gin.RoutesInfo GVA_ACTIVE_DBNAME *string + GVA_MCP_SERVER *server.MCPServer BlackCache local_cache.Cache lock sync.RWMutex ) diff --git a/server/go.mod b/server/go.mod index 9422baa881..b70c6f5661 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,14 +1,16 @@ 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 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 @@ -55,7 +57,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 @@ -111,9 +113,11 @@ 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 + 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 +125,8 @@ 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 github.com/pierrec/lz4/v4 v4.1.22 // indirect @@ -141,6 +146,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 +160,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 0c82192c12..dba8883f00 100644 --- a/server/go.sum +++ b/server/go.sum @@ -46,8 +46,10 @@ 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= 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= @@ -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= @@ -326,6 +328,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 +351,10 @@ 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= 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 +455,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 +490,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/init.go b/server/initialize/init.go new file mode 100644 index 0000000000..4dc48f3160 --- /dev/null +++ b/server/initialize/init.go @@ -0,0 +1,15 @@ +// 假设这是初始化逻辑的一部分 + +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +// 初始化全局函数 +func SetupHandlers() { + // 注册系统重载处理函数 + utils.GlobalSystemEvents.RegisterReloadHandler(func() error { + return Reload() + }) +} diff --git a/server/initialize/mcp.go b/server/initialize/mcp.go new file mode 100644 index 0000000000..5e03f2940f --- /dev/null +++ b/server/initialize/mcp.go @@ -0,0 +1,25 @@ +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, + ) + + global.GVA_MCP_SERVER = s + + mcpTool.RegisterAllTools(s) + + return server.NewSSEServer(s, + server.WithSSEEndpoint(config.SSEPath), + server.WithMessageEndpoint(config.MessagePath), + server.WithBaseURL(config.UrlPrefix)) +} diff --git a/server/initialize/reload.go b/server/initialize/reload.go new file mode 100644 index 0000000000..8fd27e6913 --- /dev/null +++ b/server/initialize/reload.go @@ -0,0 +1,45 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +// Reload 优雅地重新加载系统配置 +func Reload() error { + 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 + } + } + + // 重新建立数据库连接 + global.GVA_DB = Gorm() + + // 重新初始化其他配置 + OtherInit() + DBList() + + if global.GVA_DB != nil { + // 确保数据库表结构是最新的 + RegisterTables() + } + + // 重新初始化定时任务 + Timer() + + global.GVA_LOG.Info("系统配置重新加载完成") + return nil +} diff --git a/server/initialize/router.go b/server/initialize/router.go index c3dfe1ebb3..4e6fd083e4 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -40,6 +40,17 @@ func Routers() *gin.Engine { Router.Use(gin.Logger()) } + sseServer := McpRun() + + // 注册mcp服务 + Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) { + sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request) + }) + + Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) { + sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request) + }) + systemRouter := router.RouterGroupApp.System exampleRouter := router.RouterGroupApp.Example // 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的 diff --git a/server/main.go b/server/main.go index deaac4bad4..48ff6d91eb 100644 --- a/server/main.go +++ b/server/main.go @@ -21,13 +21,22 @@ import ( // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 -// @version v2.8.1 +// @version v2.8.2 // @description 使用gin+vue进行极速开发的全栈开发基础平台 // @securityDefinitions.apikey ApiKeyAuth // @in header // @name x-token // @BasePath / func main() { + // 初始化系统 + initializeSystem() + // 运行服务器 + core.RunServer() +} + +// initializeSystem 初始化系统所有组件 +// 提取为单独函数以便于系统重载时调用 +func initializeSystem() { global.GVA_VP = core.Viper() // 初始化Viper initialize.OtherInit() global.GVA_LOG = core.Zap() // 初始化zap日志库 @@ -35,11 +44,9 @@ func main() { global.GVA_DB = initialize.Gorm() // gorm连接数据库 initialize.Timer() initialize.DBList() + initialize.SetupHandlers() // 注册全局函数 + initialize.McpRun() if global.GVA_DB != nil { initialize.RegisterTables() // 初始化表 - // 程序结束前关闭数据库链接 - db, _ := global.GVA_DB.DB() - defer db.Close() } - core.RunWindowsServer() } 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/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("-------------------") + } +} diff --git a/server/mcp/current_time.go b/server/mcp/current_time.go new file mode 100644 index 0000000000..d6f0fc7a8a --- /dev/null +++ b/server/mcp/current_time.go @@ -0,0 +1,39 @@ +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("时区"), + mcp.Enum("UTC", "CST", "PST", "EST", "GMT", "CET", "JST", "MST", "IST", "AST", "HST"), + )) +} 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..029a49a109 --- /dev/null +++ b/server/mcp/get_nickname.go @@ -0,0 +1,71 @@ +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"), + )) +} + +// Handle 处理获取昵称的请求 +func (t *GetNickname) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 1. 参数验证 + username, ok := request.Params.Arguments["username"].(string) + if !ok { + return nil, errors.New("参数错误:username必须是字符串类型") + } + + if username == "" { + return nil, errors.New("参数错误:username不能为空") + } + + // 2. 记录操作日志 + global.GVA_LOG.Info("getNickname工具被调用", "username", username) + + // 3. 优化查询,只选择需要的字段 + var user struct { + NickName string + } + + err := global.GVA_DB.Model(&system.SysUser{}). + Select("nick_name"). + Where("username = ?", username). + First(&user).Error + + // 4. 优化错误处理 + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户不存在") + } + global.GVA_LOG.Error("数据库查询错误", "error", err) + return nil, errors.New("系统错误,请稍后再试") + } + + // 构造回复信息 + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("用户 %s 的昵称是 %s", username, user.NickName), + }, + }, + }, 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/model/system/request/sys_auto_code_mcp.go b/server/model/system/request/sys_auto_code_mcp.go new file mode 100644 index 0000000000..a52ec7c500 --- /dev/null +++ b/server/model/system/request/sys_auto_code_mcp.go @@ -0,0 +1,16 @@ +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"` + Default string `json:"default" form:"default"` + } `json:"params" form:"params"` + Response []struct { + Type string `json:"type" form:"type" binding:"required"` // text, image + } `json:"response" form:"response"` +} diff --git a/server/resource/mcp/tools.tpl b/server/resource/mcp/tools.tpl new file mode 100644 index 0000000000..1c6903a22b --- /dev/null +++ b/server/resource/mcp/tools.tpl @@ -0,0 +1,56 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" +) + +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/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/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/auto_code_mcp.go b/server/service/system/auto_code_mcp.go new file mode 100644 index 0000000000..3b6eb84181 --- /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) (toolFilePath string, 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 + } + + fileName := utils.HumpToUnderscore(info.Name) + + toolFilePath = filepath.Join(mcpToolPath, fileName+".go") + + f, err := os.Create(toolFilePath) + if err != nil { + return + } + defer f.Close() + + // 执行模板,将内容写入文件 + err = files.Execute(f, info) + if err != nil { + return + } + + return + +} diff --git a/server/service/system/auto_code_package.go b/server/service/system/auto_code_package.go index 6c3f4eee43..4e184b1cc7 100644 --- a/server/service/system/auto_code_package.go +++ b/server/service/system/auto_code_package.go @@ -233,6 +233,9 @@ func (s *autoCodePackage) Templates(ctx context.Context) ([]string, error) { if entries[i].Name() == "preview" { continue } // preview 为预览代码生成器的代码 + if entries[i].Name() == "mcp" { + continue + } // preview 为mcp生成器的代码 templates = append(templates, entries[i].Name()) } } 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/source/system/api.go b/server/source/system/api.go index fddb1d77bb..a026518696 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -117,6 +117,7 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getColumn", Description: "获取所选table的所有字段"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/installPlugin", Description: "安装插件"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/pubPlug", Description: "打包插件"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcp", Description: "自动生成 MCP Tool 模板"}, {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/createPackage", Description: "配置模板"}, {ApiGroup: "模板配置", Method: "GET", Path: "/autoCode/getTemplates", Description: "获取模板文件"}, diff --git a/server/source/system/authorities_menus.go b/server/source/system/authorities_menus.go index e6799bc33e..0bf72fd683 100644 --- a/server/source/system/authorities_menus.go +++ b/server/source/system/authorities_menus.go @@ -35,35 +35,72 @@ func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Co if !ok { return ctx, system.ErrMissingDBContext } + initAuth := &initAuthority{} authorities, ok := ctx.Value(initAuth.InitializerName()).([]sysModel.SysAuthority) if !ok { return ctx, errors.Wrap(system.ErrMissingDependentContext, "创建 [菜单-权限] 关联失败, 未找到权限表初始化数据") } - menus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu) + + allMenus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu) if !ok { return next, errors.Wrap(errors.New(""), "创建 [菜单-权限] 关联失败, 未找到菜单表初始化数据") } next = ctx - // 888 - if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(menus); err != nil { - return next, err + + // 构建菜单ID映射,方便快速查找 + menuMap := make(map[uint]sysModel.SysBaseMenu) + for _, menu := range allMenus { + menuMap[menu.ID] = menu + } + + // 为不同角色分配不同权限 + // 1. 超级管理员角色(888) - 拥有所有菜单权限 + if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(allMenus); err != nil { + return next, errors.Wrap(err, "为超级管理员分配菜单失败") + } + + // 2. 普通用户角色(8881) - 仅拥有基础功能菜单 + // 仅选择部分父级菜单及其子菜单 + var menu8881 []sysModel.SysBaseMenu + + // 添加仪表盘、关于我们和个人信息菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 && (menu.Name == "dashboard" || menu.Name == "about" || menu.Name == "person" || menu.Name == "state") { + menu8881 = append(menu8881, menu) + } } - // 8881 - menu8881 := menus[:2] - menu8881 = append(menu8881, menus[7]) if err = db.Model(&authorities[1]).Association("SysBaseMenus").Replace(menu8881); err != nil { - return next, err + return next, errors.Wrap(err, "为普通用户分配菜单失败") } - // 9528 - if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menus[:11]); err != nil { - return next, err + // 3. 测试角色(9528) - 拥有部分菜单权限 + var menu9528 []sysModel.SysBaseMenu + + // 添加所有父级菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 { + menu9528 = append(menu9528, menu) + } } - if err = db.Model(&authorities[2]).Association("SysBaseMenus").Append(menus[12:17]); err != nil { - return next, err + + // 添加部分子菜单 - 系统工具、示例文件等模块的子菜单 + for _, menu := range allMenus { + parentName := "" + if menu.ParentId > 0 && menuMap[menu.ParentId].Name != "" { + parentName = menuMap[menu.ParentId].Name + } + + if menu.ParentId > 0 && (parentName == "systemTools" || parentName == "example") { + menu9528 = append(menu9528, menu) + } + } + + if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menu9528); err != nil { + return next, errors.Wrap(err, "为测试角色分配菜单失败") } + return next, nil } diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index eda525d100..e9da7551a3 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -130,6 +130,7 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/autoCode/installPlugin", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/pubPlug", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcp", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/findSysDictionaryDetail", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/updateSysDictionaryDetail", V2: "PUT"}, diff --git a/server/source/system/menu.go b/server/source/system/menu.go index 100a2cfd2a..ba64f6a04e 100644 --- a/server/source/system/menu.go +++ b/server/source/system/menu.go @@ -50,44 +50,73 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er if !ok { return ctx, system.ErrMissingDBContext } - entities := []SysBaseMenu{ + + // 定义所有菜单 + allMenus := []SysBaseMenu{ {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "odometer"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "关于我们", Icon: "info-filled"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "user"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}}, {MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "message"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "example", Name: "example", Component: "view/example/index.vue", Sort: 7, Meta: Meta{Title: "示例文件", Icon: "management"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "系统工具", Icon: "tools"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}}, - {MenuLevel: 0, Hidden: true, ParentId: 15, Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "plugin", Name: "plugin", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "插件系统", Icon: "cherry"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}}, } - if err = db.Create(&entities).Error; err != nil { - return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"表数据初始化失败!") + + // 先创建父级菜单(ParentId = 0 的菜单) + if err = db.Create(&allMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"父级菜单初始化失败!") + } + + // 建立菜单映射 - 通过Name查找已创建的菜单及其ID + menuNameMap := make(map[string]uint) + for _, menu := range allMenus { + menuNameMap[menu.Name] = menu.ID + } + + // 定义子菜单,并设置正确的ParentId + childMenus := []SysBaseMenu{ + // superAdmin子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}}, + + // example子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}}, + + // systemTools子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: true, ParentId: menuNameMap["systemTools"], Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}}, + + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}}, + } + + // 创建子菜单 + if err = db.Create(&childMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"子菜单初始化失败!") } - next = context.WithValue(ctx, i.InitializerName(), entities) + + // 组合所有菜单作为返回结果 + allEntities := append(allMenus, childMenus...) + next = context.WithValue(ctx, i.InitializerName(), allEntities) return next, nil } 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/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/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/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/reload.go b/server/utils/reload.go deleted file mode 100644 index de5499bf33..0000000000 --- a/server/utils/reload.go +++ /dev/null @@ -1,18 +0,0 @@ -package utils - -import ( - "errors" - "os" - "os/exec" - "runtime" - "strconv" -) - -func Reload() error { - if runtime.GOOS == "windows" { - return errors.New("系统不支持") - } - pid := os.Getpid() - cmd := exec.Command("kill", "-1", strconv.Itoa(pid)) - return cmd.Run() -} 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 +} 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/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/core/config.js b/web/src/core/config.js index 709b495a25..bb44caffb3 100644 --- a/web/src/core/config.js +++ b/web/src/core/config.js @@ -17,7 +17,7 @@ export const viteLogo = (env) => { `> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin` ) ) - console.log(greenText(`> 当前版本:v2.8.1`)) + console.log(greenText(`> 当前版本:v2.8.2`)) console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`)) console.log( greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`) diff --git a/web/src/core/gin-vue-admin.js b/web/src/core/gin-vue-admin.js index 034b36b885..0823d950b9 100644 --- a/web/src/core/gin-vue-admin.js +++ b/web/src/core/gin-vue-admin.js @@ -10,7 +10,7 @@ export default { register(app) console.log(` 欢迎使用 Gin-Vue-Admin - 当前版本:v2.8.1 + 当前版本:v2.8.2 加群方式:微信:shouzi_1994 QQ群:622360840 项目地址:https://github.com/flipped-aurora/gin-vue-admin 插件市场:https://plugin.gin-vue-admin.com diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index 05c02a6cde..76707c4291 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,9 +55,9 @@ "/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/mcp.vue": "Mcp", "/src/view/systemTools/autoCode/picture.vue": "Picture", "/src/view/systemTools/autoCodeAdmin/index.vue": "AutoCodeAdmin", "/src/view/systemTools/autoPkg/autoPkg.vue": "AutoPkg", diff --git a/web/src/view/systemTools/autoCode/mcp.vue b/web/src/view/systemTools/autoCode/mcp.vue new file mode 100644 index 0000000000..16f8bc0b4e --- /dev/null +++ b/web/src/view/systemTools/autoCode/mcp.vue @@ -0,0 +1,147 @@ + + + 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'