diff --git a/.gitignore b/.gitignore index a25df7bb36..4707851efa 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ rm_file/ /server/server /server/latest_log /server/__debug_bin* +/server/*.local.yaml server/uploads/ *.iml diff --git a/server/api/v1/system/sys_dictionary.go b/server/api/v1/system/sys_dictionary.go index 1dfe9d0899..5e6ca79c3b 100644 --- a/server/api/v1/system/sys_dictionary.go +++ b/server/api/v1/system/sys_dictionary.go @@ -4,6 +4,7 @@ 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" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) @@ -116,10 +117,17 @@ func (s *DictionaryApi) FindSysDictionary(c *gin.Context) { // @Security ApiKeyAuth // @accept application/json // @Produce application/json +// @Param data query request.SysDictionarySearch true "字典 name 或者 type" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量" // @Router /sysDictionary/getSysDictionaryList [get] func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) { - list, err := dictionaryService.GetSysDictionaryInfoList() + var dictionary request.SysDictionarySearch + err := c.ShouldBindQuery(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, err := dictionaryService.GetSysDictionaryInfoList(c, dictionary) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败", c) diff --git a/server/api/v1/system/sys_dictionary_detail.go b/server/api/v1/system/sys_dictionary_detail.go index 754af1be6a..6be069f818 100644 --- a/server/api/v1/system/sys_dictionary_detail.go +++ b/server/api/v1/system/sys_dictionary_detail.go @@ -1,6 +1,8 @@ package system import ( + "strconv" + "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" @@ -146,3 +148,120 @@ func (s *DictionaryDetailApi) GetSysDictionaryDetailList(c *gin.Context) { PageSize: pageInfo.PageSize, }, "获取成功", c) } + +// GetDictionaryTreeList +// @Tags SysDictionaryDetail +// @Summary 获取字典详情树形结构 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param sysDictionaryID query int true "字典ID" +// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构" +// @Router /sysDictionaryDetail/getDictionaryTreeList [get] +func (s *DictionaryDetailApi) GetDictionaryTreeList(c *gin.Context) { + sysDictionaryID := c.Query("sysDictionaryID") + if sysDictionaryID == "" { + response.FailWithMessage("字典ID不能为空", c) + return + } + + var id uint + if idUint64, err := strconv.ParseUint(sysDictionaryID, 10, 32); err != nil { + response.FailWithMessage("字典ID格式错误", c) + return + } else { + id = uint(idUint64) + } + + list, err := dictionaryDetailService.GetDictionaryTreeList(id) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"list": list}, "获取成功", c) +} + +// GetDictionaryTreeListByType +// @Tags SysDictionaryDetail +// @Summary 根据字典类型获取字典详情树形结构 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param type query string true "字典类型" +// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构" +// @Router /sysDictionaryDetail/getDictionaryTreeListByType [get] +func (s *DictionaryDetailApi) GetDictionaryTreeListByType(c *gin.Context) { + dictType := c.Query("type") + if dictType == "" { + response.FailWithMessage("字典类型不能为空", c) + return + } + + list, err := dictionaryDetailService.GetDictionaryTreeListByType(dictType) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"list": list}, "获取成功", c) +} + +// GetDictionaryDetailsByParent +// @Tags SysDictionaryDetail +// @Summary 根据父级ID获取字典详情 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.GetDictionaryDetailsByParentRequest true "查询参数" +// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情列表" +// @Router /sysDictionaryDetail/getDictionaryDetailsByParent [get] +func (s *DictionaryDetailApi) GetDictionaryDetailsByParent(c *gin.Context) { + var req request.GetDictionaryDetailsByParentRequest + err := c.ShouldBindQuery(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + list, err := dictionaryDetailService.GetDictionaryDetailsByParent(req) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"list": list}, "获取成功", c) +} + +// GetDictionaryPath +// @Tags SysDictionaryDetail +// @Summary 获取字典详情的完整路径 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param id query uint true "字典详情ID" +// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情路径" +// @Router /sysDictionaryDetail/getDictionaryPath [get] +func (s *DictionaryDetailApi) GetDictionaryPath(c *gin.Context) { + idStr := c.Query("id") + if idStr == "" { + response.FailWithMessage("字典详情ID不能为空", c) + return + } + + var id uint + if idUint64, err := strconv.ParseUint(idStr, 10, 32); err != nil { + response.FailWithMessage("字典详情ID格式错误", c) + return + } else { + id = uint(idUint64) + } + + path, err := dictionaryDetailService.GetDictionaryPath(id) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"path": path}, "获取成功", c) +} diff --git a/server/config.docker.yaml b/server/config.docker.yaml index 662c5cad1d..3a3368205d 100644 --- a/server/config.docker.yaml +++ b/server/config.docker.yaml @@ -280,4 +280,6 @@ mcp: version: v1.0.0 sse_path: /sse message_path: /message - url_prefix: '' \ No newline at end of file + url_prefix: '' + addr: 8889 + separate: false diff --git a/server/config.yaml b/server/config.yaml index 35aaba3d05..f1c1eea0d7 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -87,6 +87,8 @@ system: router-prefix: "" # 严格角色模式 打开后权限将会存在上下级关系 use-strict-auth: false + # 自动迁移数据库表结构,生产环境建议设为false,手动迁移 + disable-auto-migrate: false # captcha configuration captcha: @@ -268,7 +270,6 @@ cors: allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id allow-methods: POST, GET expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true # 布尔值 - allow-origin: example2.com allow-headers: content-type @@ -280,4 +281,6 @@ mcp: version: v1.0.0 sse_path: /sse message_path: /message - url_prefix: '' \ No newline at end of file + url_prefix: '' + addr: 8889 + separate: false diff --git a/server/config/mcp.go b/server/config/mcp.go index 81f4bff307..15a78760d0 100644 --- a/server/config/mcp.go +++ b/server/config/mcp.go @@ -6,4 +6,6 @@ type MCP struct { 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前缀 + Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 独立MCP服务端口 + Separate bool `mapstructure:"separate" json:"separate" yaml:"separate"` // 是否独立运行MCP服务 } diff --git a/server/config/system.go b/server/config/system.go index b47dbf64e6..3036143edb 100644 --- a/server/config/system.go +++ b/server/config/system.go @@ -11,4 +11,5 @@ type System struct { UseRedis bool `mapstructure:"use-redis" json:"use-redis" yaml:"use-redis"` // 使用redis UseMongo bool `mapstructure:"use-mongo" json:"use-mongo" yaml:"use-mongo"` // 使用mongo UseStrictAuth bool `mapstructure:"use-strict-auth" json:"use-strict-auth" yaml:"use-strict-auth"` // 使用树形角色分配模式 + DisableAutoMigrate bool `mapstructure:"disable-auto-migrate" json:"disable-auto-migrate" yaml:"disable-auto-migrate"` // 自动迁移数据库表结构,生产环境建议设为false,手动迁移 } diff --git a/server/go.mod b/server/go.mod index b3b8dfe44a..a937598278 100644 --- a/server/go.mod +++ b/server/go.mod @@ -20,7 +20,7 @@ require ( github.com/gookit/color v1.5.4 github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible - github.com/mark3labs/mcp-go v0.31.0 + github.com/mark3labs/mcp-go v0.41.1 github.com/mholt/archives v0.1.1 github.com/minio/minio-go/v7 v7.0.84 github.com/mojocn/base64Captcha v1.3.8 @@ -61,10 +61,12 @@ require ( 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/bahlo/generic-list-go v0.2.0 // indirect github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.0 // indirect github.com/bodgit/windows v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/sonic v1.12.7 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/casbin/govaluate v1.3.0 // indirect @@ -99,6 +101,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect @@ -153,6 +156,7 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/ulikunitz/xz v0.5.12 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/server/go.sum b/server/go.sum index 4b7359460b..33301c5a26 100644 --- a/server/go.sum +++ b/server/go.sum @@ -56,6 +56,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= @@ -69,6 +71,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -247,6 +251,8 @@ github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible h1:XQVXdk+WAJ github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -315,8 +321,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.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4= -github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= +github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= 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= @@ -473,6 +479,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go index 45d8a3145a..2aacb84ab6 100644 --- a/server/initialize/gorm.go +++ b/server/initialize/gorm.go @@ -35,6 +35,11 @@ func Gorm() *gorm.DB { } func RegisterTables() { + if global.GVA_CONFIG.System.DisableAutoMigrate { + global.GVA_LOG.Info("auto-migrate is disabled, skipping table registration") + return + } + db := global.GVA_DB err := db.AutoMigrate( diff --git a/server/initialize/router.go b/server/initialize/router.go index 11ebf420f3..d297df8746 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -40,16 +40,19 @@ func Routers() *gin.Engine { Router.Use(gin.Logger()) } - sseServer := McpRun() + if !global.GVA_CONFIG.MCP.Separate { - // 注册mcp服务 - Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) { - sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request) - }) + sseServer := McpRun() - Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) { - sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request) - }) + // 注册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 diff --git a/server/mcp/api_creator.go b/server/mcp/api_creator.go index 1a1503d916..22009f6cac 100644 --- a/server/mcp/api_creator.go +++ b/server/mcp/api_creator.go @@ -180,21 +180,11 @@ func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (* return nil, fmt.Errorf("序列化结果失败: %v", err) } - // 添加权限分配提醒 - permissionReminder := "\n\n⚠️ 重要提醒:\n" + - "API创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的API权限," + - "以确保用户能够正常访问新接口。\n" + - "具体步骤:\n" + - "1. 进入角色管理页面\n" + - "2. 选择需要授权的角色\n" + - "3. 在API权限中勾选新创建的API接口\n" + - "4. 保存权限配置" - return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", - Text: fmt.Sprintf("API创建结果:\n\n%s%s", string(resultJSON), permissionReminder), + Text: fmt.Sprintf("API创建结果:\n\n%s", string(resultJSON)), }, }, }, nil diff --git a/server/mcp/dictionary_generator.go b/server/mcp/dictionary_generator.go index e7c6558064..ea548fdfd0 100644 --- a/server/mcp/dictionary_generator.go +++ b/server/mcp/dictionary_generator.go @@ -21,6 +21,13 @@ func init() { // DictionaryOptionsGenerator 字典选项生成器 type DictionaryOptionsGenerator struct{} +// DictionaryOption 字典选项结构 +type DictionaryOption struct { + Label string `json:"label"` + Value string `json:"value"` + Sort int `json:"sort"` +} + // DictionaryGenerateRequest 字典生成请求 type DictionaryGenerateRequest struct { DictType string `json:"dictType"` // 字典类型 @@ -139,11 +146,11 @@ func (d *DictionaryOptionsGenerator) InputSchema() map[string]interface{} { }, "dictName": map[string]interface{}{ "type": "string", - "description": "字典名称,可选,默认根据fieldDesc生成", + "description": "字典名称,必填,默认根据fieldDesc生成", }, "description": map[string]interface{}{ "type": "string", - "description": "字典描述,可选", + "description": "字典描述,必填", }, }, "required": []string{"dictType", "fieldDesc", "options"}, @@ -180,7 +187,6 @@ func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.Cal return nil, errors.New("options 不能为空") } - // 可选参数 dictName, _ := args["dictName"].(string) description, _ := args["description"].(string) diff --git a/server/mcp/dictionary_query.go b/server/mcp/dictionary_query.go index aea5df48b9..0e0f175513 100644 --- a/server/mcp/dictionary_query.go +++ b/server/mcp/dictionary_query.go @@ -18,13 +18,18 @@ func init() { RegisterTool(&DictionaryQuery{}) } +type DictionaryPre struct { + Type string `json:"type"` // 字典名(英) + Desc string `json:"desc"` // 描述 +} + // DictionaryInfo 字典信息结构 type DictionaryInfo struct { - ID uint `json:"id"` - Name string `json:"name"` // 字典名(中) - Type string `json:"type"` // 字典名(英) - Status *bool `json:"status"` // 状态 - Desc string `json:"desc"` // 描述 + ID uint `json:"id"` + Name string `json:"name"` // 字典名(中) + Type string `json:"type"` // 字典名(英) + Status *bool `json:"status"` // 状态 + Desc string `json:"desc"` // 描述 Details []DictionaryDetailInfo `json:"details"` // 字典详情 } @@ -40,9 +45,9 @@ type DictionaryDetailInfo struct { // DictionaryQueryResponse 字典查询响应结构 type DictionaryQueryResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Total int `json:"total"` + Success bool `json:"success"` + Message string `json:"message"` + Total int `json:"total"` Dictionaries []DictionaryInfo `json:"dictionaries"` } @@ -68,36 +73,36 @@ func (d *DictionaryQuery) New() mcp.Tool { // Handle 处理字典查询请求 func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := request.GetArguments() - + // 获取参数 dictType := "" if val, ok := args["dictType"].(string); ok { dictType = val } - + includeDisabled := false if val, ok := args["includeDisabled"].(bool); ok { includeDisabled = val } - + detailsOnly := false if val, ok := args["detailsOnly"].(bool); ok { detailsOnly = val } - + // 获取字典服务 dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService - + var dictionaries []DictionaryInfo var err error - + if dictType != "" { // 查询指定类型的字典 var status *bool if !includeDisabled { status = &[]bool{true}[0] } - + sysDictionary, err := dictionaryService.GetSysDictionary(dictType, 0, status) if err != nil { global.GVA_LOG.Error("查询字典失败", zap.Error(err)) @@ -107,7 +112,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques }, }, nil } - + // 转换为响应格式 dictInfo := DictionaryInfo{ ID: sysDictionary.ID, @@ -116,7 +121,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques Status: sysDictionary.Status, Desc: sysDictionary.Desc, } - + // 获取字典详情 for _, detail := range sysDictionary.SysDictionaryDetails { if includeDisabled || (detail.Status != nil && *detail.Status) { @@ -130,17 +135,17 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques }) } } - + dictionaries = append(dictionaries, dictInfo) } else { // 查询所有字典 var sysDictionaries []system.SysDictionary db := global.GVA_DB.Model(&system.SysDictionary{}) - + if !includeDisabled { db = db.Where("status = ?", true) } - + err = db.Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { if includeDisabled { return db.Order("sort") @@ -148,7 +153,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques return db.Where("status = ?", true).Order("sort") } }).Find(&sysDictionaries).Error - + if err != nil { global.GVA_LOG.Error("查询字典列表失败", zap.Error(err)) return &mcp.CallToolResult{ @@ -157,7 +162,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques }, }, nil } - + // 转换为响应格式 for _, dict := range sysDictionaries { dictInfo := DictionaryInfo{ @@ -167,7 +172,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques Status: dict.Status, Desc: dict.Desc, } - + // 获取字典详情 for _, detail := range dict.SysDictionaryDetails { if includeDisabled || (detail.Status != nil && *detail.Status) { @@ -181,25 +186,25 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques }) } } - + dictionaries = append(dictionaries, dictInfo) } } - + // 如果只需要详情信息,则提取所有详情 if detailsOnly { var allDetails []DictionaryDetailInfo for _, dict := range dictionaries { allDetails = append(allDetails, dict.Details...) } - + response := map[string]interface{}{ "success": true, "message": "查询字典详情成功", "total": len(allDetails), "details": allDetails, } - + responseJSON, _ := json.Marshal(response) return &mcp.CallToolResult{ Content: []mcp.Content{ @@ -207,7 +212,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques }, }, nil } - + // 构建响应 response := DictionaryQueryResponse{ Success: true, @@ -215,7 +220,7 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques Total: len(dictionaries), Dictionaries: dictionaries, } - + responseJSON, err := json.Marshal(response) if err != nil { global.GVA_LOG.Error("序列化响应失败", zap.Error(err)) @@ -225,10 +230,10 @@ func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolReques }, }, nil } - + return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.NewTextContent(string(responseJSON)), }, }, nil -} \ No newline at end of file +} diff --git a/server/mcp/execution_plan_schema.md b/server/mcp/execution_plan_schema.md deleted file mode 100644 index 622e83d099..0000000000 --- a/server/mcp/execution_plan_schema.md +++ /dev/null @@ -1,529 +0,0 @@ -# ExecutionPlan 结构体格式说明 - -## 概述 -ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了创建包和模块所需的所有信息。 - -## 完整结构体定义 - -```go -type ExecutionPlan struct { - PackageName string `json:"packageName"` // 包名,如:"user", "order", "product" - PackageType string `json:"packageType"` // "plugin" 或 "package" - NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包 - NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块 - PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息(当NeedCreatedPackage=true时必需) - ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组(当NeedCreatedModules=true时必需,支持批量创建) - Paths map[string]string `json:"paths,omitempty"` // 路径信息 -} -``` - -## 子结构体详细说明 - -### 1. SysAutoCodePackageCreate 结构体 - -```go -type SysAutoCodePackageCreate struct { - Desc string `json:"desc"` // 描述,如:"用户管理模块" - Label string `json:"label"` // 展示名,如:"用户管理" - Template string `json:"template"` // 模板类型:"plugin" 或 "package" - PackageName string `json:"packageName"` // 包名,如:"user" - Module string `json:"-"` // 模块名(自动填充,无需设置) -} -``` - -### 2. AutoCode 结构体(核心字段) - -```go -type AutoCode struct { - Package string `json:"package"` // 包名 - TableName string `json:"tableName"` // 数据库表名 - BusinessDB string `json:"businessDB"` // 业务数据库名 - StructName string `json:"structName"` // 结构体名称 - PackageName string `json:"packageName"` // 文件名称 - Description string `json:"description"` // 结构体中文名称 - Abbreviation string `json:"abbreviation"` // 结构体简称 - HumpPackageName string `json:"humpPackageName"` // 驼峰命名的包名 - GvaModel bool `json:"gvaModel"` // 是否使用GVA默认Model - AutoMigrate bool `json:"autoMigrate"` // 是否自动迁移表结构 - AutoCreateResource bool `json:"autoCreateResource"` // 是否自动创建资源标识 - AutoCreateApiToSql bool `json:"autoCreateApiToSql"` // 是否自动创建API - AutoCreateMenuToSql bool `json:"autoCreateMenuToSql"` // 是否自动创建菜单 - AutoCreateBtnAuth bool `json:"autoCreateBtnAuth"` // 是否自动创建按钮权限 - OnlyTemplate bool `json:"onlyTemplate"` // 是否只生成模板 - IsTree bool `json:"isTree"` // 是否树形结构 - TreeJson string `json:"treeJson"` // 树形结构JSON字段 - IsAdd bool `json:"isAdd"` // 是否新增 - Fields []*AutoCodeField `json:"fields"` // 字段列表 - GenerateWeb bool `json:"generateWeb"` // 是否生成前端代码 - GenerateServer bool `json:"generateServer"` // 是否生成后端代码 - Module string `json:"-"` // 模块(自动填充) - DictTypes []string `json:"-"` // 字典类型(自动填充) -} -``` - -### 3. AutoCodeField 结构体(字段定义) - -```go -type AutoCodeField struct { - FieldName string `json:"fieldName"` // 字段名 - FieldDesc string `json:"fieldDesc"` // 字段中文描述 - FieldType string `json:"fieldType"` // 字段类型:string, int, bool, time.Time等 - FieldJson string `json:"fieldJson"` // JSON标签名 - DataTypeLong string `json:"dataTypeLong"` // 数据库字段长度 - Comment string `json:"comment"` // 数据库字段注释 - ColumnName string `json:"columnName"` // 数据库列名 - FieldSearchType string `json:"fieldSearchType"` // 搜索类型:EQ, LIKE, BETWEEN等 - FieldSearchHide bool `json:"fieldSearchHide"` // 是否隐藏查询条件 - DictType string `json:"dictType"` // 字典类型 - Form bool `json:"form"` // 是否在表单中显示 - Table bool `json:"table"` // 是否在表格中显示 - Desc bool `json:"desc"` // 是否在详情中显示 - Excel bool `json:"excel"` // 是否支持导入导出 - Require bool `json:"require"` // 是否必填 - DefaultValue string `json:"defaultValue"` // 默认值 - ErrorText string `json:"errorText"` // 校验失败提示 - Clearable bool `json:"clearable"` // 是否可清空 - Sort bool `json:"sort"` // 是否支持排序 - PrimaryKey bool `json:"primaryKey"` // 是否主键 - DataSource *DataSource `json:"dataSource"` // 数据源配置(用于关联其他表) - CheckDataSource bool `json:"checkDataSource"` // 是否检查数据源 - FieldIndexType string `json:"fieldIndexType"` // 索引类型 -} -``` - -### 4. DataSource 结构体(关联表配置) - -```go -type DataSource struct { - DBName string `json:"dbName"` // 关联的数据库名称 - Table string `json:"table"` // 关联的表名 - Label string `json:"label"` // 用于显示的字段名(如name、title等) - Value string `json:"value"` // 用于存储的值字段名(通常是id) - Association int `json:"association"` // 关联关系:1=一对一,2=一对多 - HasDeletedAt bool `json:"hasDeletedAt"` // 关联表是否有软删除字段 -} -``` - -## 使用示例 - -### 示例1:创建新包和批量创建多个模块 - -```json -{ - "packageName": "user", - "packageType": "package", - "needCreatedPackage": true, - "needCreatedModules": true, - "packageInfo": { - "desc": "用户管理模块", - "label": "用户管理", - "template": "package", - "packageName": "user" - }, - "modulesInfo": [ - { - "package": "user", - "tableName": "sys_users", - "businessDB": "", - "structName": "User", - "packageName": "user", - "description": "用户", - "abbreviation": "user", - "humpPackageName": "user", - "gvaModel": true, - "autoMigrate": true, - "autoCreateResource": true, - "autoCreateApiToSql": true, - "autoCreateMenuToSql": true, - "autoCreateBtnAuth": true, - "onlyTemplate": false, - "isTree": false, - "treeJson": "", - "isAdd": true, - "generateWeb": true, - "generateServer": true, - "fields": [ - { - "fieldName": "Username", - "fieldDesc": "用户名", - "fieldType": "string", - "fieldJson": "username", - "dataTypeLong": "50", - "comment": "用户名", - "columnName": "username", - "fieldSearchType": "LIKE", - "fieldSearchHide": false, - "dictType": "", - "form": true, - "table": true, - "desc": true, - "excel": true, - "require": true, - "defaultValue": "", - "errorText": "请输入用户名", - "clearable": true, - "sort": false, - "primaryKey": false, - "dataSource": { - "dbName": "gva", - "table": "sys_users", - "label": "username", - "value": "id", - "association": 2, - "hasDeletedAt": true - }, - "checkDataSource": true, - "fieldIndexType": "" - }, - { - "fieldName": "Email", - "fieldDesc": "邮箱", - "fieldType": "string", - "fieldJson": "email", - "dataTypeLong": "100", - "comment": "邮箱地址", - "columnName": "email", - "fieldSearchType": "EQ", - "fieldSearchHide": false, - "dictType": "", - "form": true, - "table": true, - "desc": true, - "excel": true, - "require": true, - "defaultValue": "", - "errorText": "请输入邮箱", - "clearable": true, - "sort": false, - "primaryKey": false, - "dataSource": null, - "checkDataSource": false, - "fieldIndexType": "index" - } - ] - }, - { - "package": "user", - "tableName": "user_profiles", - "businessDB": "", - "structName": "UserProfile", - "packageName": "user", - "description": "用户档案", - "abbreviation": "userProfile", - "humpPackageName": "user", - "gvaModel": true, - "autoMigrate": true, - "autoCreateResource": true, - "autoCreateApiToSql": true, - "autoCreateMenuToSql": true, - "autoCreateBtnAuth": true, - "onlyTemplate": false, - "isTree": false, - "treeJson": "", - "isAdd": true, - "generateWeb": true, - "generateServer": true, - "fields": [ - { - "fieldName": "UserID", - "fieldDesc": "用户ID", - "fieldType": "int", - "fieldJson": "userId", - "dataTypeLong": "", - "comment": "关联用户ID", - "columnName": "user_id", - "fieldSearchType": "EQ", - "fieldSearchHide": false, - "dictType": "", - "form": true, - "table": true, - "desc": true, - "excel": true, - "require": true, - "defaultValue": "", - "errorText": "请选择用户", - "clearable": true, - "sort": false, - "primaryKey": false, - "dataSource": null, - "checkDataSource": false, - "fieldIndexType": "index" - }, - { - "fieldName": "Avatar", - "fieldDesc": "头像", - "fieldType": "string", - "fieldJson": "avatar", - "dataTypeLong": "255", - "comment": "用户头像URL", - "columnName": "avatar", - "fieldSearchType": "", - "fieldSearchHide": true, - "dictType": "", - "form": true, - "table": true, - "desc": true, - "excel": false, - "require": false, - "defaultValue": "", - "errorText": "", - "clearable": true, - "sort": false, - "primaryKey": false, - "dataSource": null, - "checkDataSource": false, - "fieldIndexType": "" - } - ] - } - ] -} -``` - -### 示例2:仅在现有包中批量创建多个模块 - -```json -{ - "packageName": "system", - "packageType": "package", - "needCreatedPackage": false, - "needCreatedModules": true, - "packageInfo": null, - "modulesInfo": [ - { - "package": "system", - "tableName": "sys_roles", - "businessDB": "", - "structName": "Role", - "packageName": "system", - "description": "角色", - "abbreviation": "role", - "humpPackageName": "system", - "gvaModel": true, - "autoMigrate": true, - "autoCreateResource": true, - "autoCreateApiToSql": true, - "autoCreateMenuToSql": true, - "autoCreateBtnAuth": true, - "onlyTemplate": false, - "isTree": false, - "generateWeb": true, - "generateServer": true, - "fields": [ - { - "fieldName": "RoleName", - "fieldDesc": "角色名称", - "fieldType": "string", - "fieldJson": "roleName", - "dataTypeLong": "50", - "comment": "角色名称", - "columnName": "role_name", - "fieldSearchType": "LIKE", - "form": true, - "table": true, - "desc": true, - "require": true - } - ] - }, - { - "package": "system", - "tableName": "sys_permissions", - "businessDB": "", - "structName": "Permission", - "packageName": "system", - "description": "权限", - "abbreviation": "permission", - "humpPackageName": "system", - "gvaModel": true, - "autoMigrate": true, - "autoCreateResource": true, - "autoCreateApiToSql": true, - "autoCreateMenuToSql": true, - "autoCreateBtnAuth": true, - "onlyTemplate": false, - "isTree": false, - "generateWeb": true, - "generateServer": true, - "fields": [ - { - "fieldName": "PermissionName", - "fieldDesc": "权限名称", - "fieldType": "string", - "fieldJson": "permissionName", - "dataTypeLong": "100", - "comment": "权限名称", - "columnName": "permission_name", - "fieldSearchType": "LIKE", - "form": true, - "table": true, - "desc": true, - "require": true - }, - { - "fieldName": "PermissionCode", - "fieldDesc": "权限代码", - "fieldType": "string", - "fieldJson": "permissionCode", - "dataTypeLong": "50", - "comment": "权限代码", - "columnName": "permission_code", - "fieldSearchType": "=", - "form": true, - "table": true, - "desc": true, - "require": true - } - ] - } - ] -} -``` - -### 示例3:模块关联关系配置详解 - -以下示例展示了如何配置不同类型的关联关系: - -```json -{ - "packageName": "order", - "packageType": "package", - "needCreatedPackage": true, - "needCreatedModules": true, - "packageInfo": { - "desc": "订单管理模块", - "label": "订单管理", - "template": "package", - "packageName": "order" - }, - "modulesInfo": [ - { - "package": "order", - "tableName": "orders", - "structName": "Order", - "packageName": "order", - "description": "订单", - "abbreviation": "order", - "humpPackageName": "order", - "gvaModel": true, - "autoMigrate": true, - "autoCreateResource": true, - "autoCreateApiToSql": true, - "autoCreateMenuToSql": true, - "autoCreateBtnAuth": true, - "generateWeb": true, - "generateServer": true, - "fields": [ - { - "fieldName": "UserID", - "fieldDesc": "下单用户", - "fieldType": "uint", - "fieldJson": "userId", - "columnName": "user_id", - "fieldSearchType": "EQ", - "form": true, - "table": true, - "desc": true, - "require": true, - "dataSource": { - "dbName": "gva", - "table": "sys_users", - "label": "username", - "value": "id", - "association": 2, - "hasDeletedAt": true - }, - "checkDataSource": true - }, - { - "fieldName": "ProductID", - "fieldDesc": "商品", - "fieldType": "uint", - "fieldJson": "productId", - "columnName": "product_id", - "fieldSearchType": "EQ", - "form": true, - "table": true, - "desc": true, - "require": true, - "dataSource": { - "dbName": "gva", - "table": "products", - "label": "name", - "value": "id", - "association": 2, - "hasDeletedAt": false - }, - "checkDataSource": true - }, - { - "fieldName": "Status", - "fieldDesc": "订单状态", - "fieldType": "int", - "fieldJson": "status", - "columnName": "status", - "fieldSearchType": "EQ", - "form": true, - "table": true, - "desc": true, - "require": true, - "dictType": "order_status" - } - ] - } - ] -} -``` - -## DataSource 配置说明 - -### 关联关系类型 -- **association: 1** - 一对一关联(如用户与用户档案) -- **association: 2** - 一对多关联(如用户与订单) - -### 配置要点 -1. **dbName**: 通常为 "gva"(默认数据库) -2. **table**: 关联表的实际表名 -3. **label**: 用于前端显示的字段(如用户名、商品名称) -4. **value**: 用于存储关联ID的字段(通常是 "id") -5. **hasDeletedAt**: 关联表是否支持软删除 -6. **checkDataSource**: 建议设为true,会验证关联表是否存在 - -### 常见关联场景 -- 用户关联:`{"table": "sys_users", "label": "username", "value": "id"}` -- 角色关联:`{"table": "sys_authorities", "label": "authorityName", "value": "authorityId"}` -- 部门关联:`{"table": "sys_departments", "label": "name", "value": "id"}` -- 分类关联:`{"table": "categories", "label": "name", "value": "id"}` - -## 重要注意事项 - -1. **PackageType**: 只能是 "plugin" 或 "package" -2. **NeedCreatedPackage**: 当为true时,PackageInfo必须提供 -3. **NeedCreatedModules**: 当为true时,ModulesInfo必须提供 -4. **字段类型**: FieldType支持的类型包括: - - string(字符串) - - richtext(富文本) - - int(整型) - - bool(布尔值) - - float64(浮点型) - - time.Time(时间) - - enum(枚举) - - picture(单图片,字符串) - - pictures(多图片,json字符串) - - video(视频,字符串) - - file(文件,json字符串) - - json(JSON) - - array(数组) -5. **搜索类型**: FieldSearchType支持:EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等 -6. **索引类型**: FieldIndexType支持:index, unique等 -7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段 -8. **关联配置**: 使用dataSource时,确保关联表已存在,建议开启checkDataSource验证 - -## 常见错误避免 - -1. 确保PackageName和ModuleName符合Go语言命名规范 -2. 字段名使用大写开头的驼峰命名 -3. JSON标签使用小写开头的驼峰命名 -4. 数据库列名使用下划线分隔的小写命名 -5. 必填字段不要遗漏 -6. 字段类型要与实际需求匹配 \ No newline at end of file diff --git a/server/mcp/gag_usage_example.md b/server/mcp/gag_usage_example.md deleted file mode 100644 index cfc7fdcb34..0000000000 --- a/server/mcp/gag_usage_example.md +++ /dev/null @@ -1,205 +0,0 @@ -# GAG工具使用示例 - 带用户确认流程 - -## 新的工作流程 - -现在GAG工具支持三步工作流程: -1. `analyze` - 分析现有模块信息 -2. `confirm` - 请求用户确认创建计划 -3. `execute` - 执行创建操作(需要用户确认) - -## 使用示例 - -### 第一步:分析 -```json -{ - "action": "analyze", - "requirement": "创建一个图书管理功能" -} -``` - -### 第二步:确认(支持批量创建多个模块) -```json -{ - "action": "confirm", - "executionPlan": { - "packageName": "library", - "packageType": "package", - "needCreatedPackage": true, - "needCreatedModules": true, - "packageInfo": { - "desc": "图书管理包", - "label": "图书管理", - "template": "package", - "packageName": "library" - }, - "modulesInfo": [ - { - "package": "library", - "tableName": "library_books", - "businessDB": "", - "structName": "Book", - "packageName": "library", - "description": "图书信息", - "abbreviation": "book", - "humpPackageName": "Library", - "gvaModel": true, - "autoMigrate": true, - "autoCreateResource": true, - "autoCreateApiToSql": true, - "autoCreateMenuToSql": true, - "autoCreateBtnAuth": true, - "onlyTemplate": false, - "isTree": false, - "treeJson": "", - "isAdd": false, - "generateWeb": true, - "generateServer": true, - "fields": [ - { - "fieldName": "title", - "fieldDesc": "书名", - "fieldType": "string", - "fieldJson": "title", - "dataTypeLong": "255", - "comment": "书名", - "columnName": "title", - "fieldSearchType": "LIKE", - "fieldSearchHide": false, - "dictType": "", - "form": true, - "table": true, - "desc": true, - "excel": true, - "require": true, - "defaultValue": "", - "errorText": "请输入书名", - "clearable": true, - "sort": false, - "primaryKey": false, - "dataSource": {}, - "checkDataSource": false, - "fieldIndexType": "" - }, - { - "fieldName": "AuthorID", - "fieldDesc": "作者", - "fieldType": "uint", - "fieldJson": "authorId", - "dataTypeLong": "", - "comment": "作者ID", - "columnName": "author_id", - "fieldSearchType": "EQ", - "fieldSearchHide": false, - "dictType": "", - "form": true, - "table": true, - "desc": true, - "excel": true, - "require": true, - "defaultValue": "", - "errorText": "请选择作者", - "clearable": true, - "sort": false, - "primaryKey": false, - "dataSource": { - "dbName": "gva", - "table": "library_authors", - "label": "name", - "value": "id", - "association": 2, - "hasDeletedAt": true - }, - "checkDataSource": true, - "fieldIndexType": "" - } - ] - }, - { - "package": "library", - "tableName": "library_authors", - "businessDB": "", - "structName": "Author", - "packageName": "library", - "description": "作者信息", - "abbreviation": "author", - "humpPackageName": "Library", - "gvaModel": true, - "autoMigrate": true, - "autoCreateResource": true, - "autoCreateApiToSql": true, - "autoCreateMenuToSql": true, - "autoCreateBtnAuth": true, - "onlyTemplate": false, - "isTree": false, - "treeJson": "", - "isAdd": false, - "generateWeb": true, - "generateServer": true, - "fields": [ - { - "fieldName": "name", - "fieldDesc": "作者姓名", - "fieldType": "string", - "fieldJson": "name", - "dataTypeLong": "100", - "comment": "作者姓名", - "columnName": "name", - "fieldSearchType": "LIKE", - "fieldSearchHide": false, - "dictType": "", - "form": true, - "table": true, - "desc": true, - "excel": true, - "require": true, - "defaultValue": "", - "errorText": "请输入作者姓名", - "clearable": true, - "sort": false, - "primaryKey": false, - "dataSource": {}, - "checkDataSource": false, - "fieldIndexType": "" - } - ] - } - ] - } -} -``` - -### 第三步:执行(需要确认参数) -```json -{ - "action": "execute", - "executionPlan": { - // ... 同上面的executionPlan - }, - "packageConfirm": "yes", // 确认创建包 - "modulesConfirm": "yes" // 确认创建模块 -} -``` - -## 确认参数说明 - -- `packageConfirm`: 当`needCreatedPackage`为true时必需 - - "yes": 确认创建包 - - "no": 取消创建包(停止后续处理) - -- `modulesConfirm`: 当`needCreatedModules`为true时必需 - - "yes": 确认创建模块 - - "no": 取消创建模块(停止后续处理) - -## 取消操作的行为 - -1. 如果用户在`packageConfirm`中选择"no",系统将停止所有后续处理 -2. 如果用户在`modulesConfirm`中选择"no",系统将停止模块创建 -3. 任何取消操作都会返回相应的取消消息,不会执行任何创建操作 - -## 注意事项 - -1. 必须先调用`confirm`来获取确认信息 -2. 在`execute`时必须提供相应的确认参数 -3. 确认参数的值必须是"yes"或"no" -4. 如果不需要创建包或模块,则不需要提供对应的确认参数 -5. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组) \ No newline at end of file diff --git a/server/mcp/gva_analyze.go b/server/mcp/gva_analyze.go new file mode 100644 index 0000000000..e48409bfe8 --- /dev/null +++ b/server/mcp/gva_analyze.go @@ -0,0 +1,475 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "os" + "path/filepath" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/mark3labs/mcp-go/mcp" +) + +// 注册工具 +func init() { + RegisterTool(&GVAAnalyzer{}) +} + +// GVAAnalyzer GVA分析器 - 用于分析当前功能是否需要创建独立的package和module +type GVAAnalyzer struct{} + +// AnalyzeRequest 分析请求结构体 +type AnalyzeRequest struct { + Requirement string `json:"requirement" binding:"required"` // 用户需求描述 +} + +// AnalyzeResponse 分析响应结构体 +type AnalyzeResponse struct { + ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息 + PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息 + Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息 + CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有) +} + +// ModuleInfo 模块信息 +type ModuleInfo struct { + ModuleName string `json:"moduleName"` // 模块名称 + PackageName string `json:"packageName"` // 包名 + Template string `json:"template"` // 模板类型 + StructName string `json:"structName"` // 结构体名称 + TableName string `json:"tableName"` // 表名 + Description string `json:"description"` // 描述 + FilePaths []string `json:"filePaths"` // 相关文件路径 +} + +// PackageInfo 包信息 +type PackageInfo struct { + PackageName string `json:"packageName"` // 包名 + Template string `json:"template"` // 模板类型 + Label string `json:"label"` // 标签 + Desc string `json:"desc"` // 描述 + Module string `json:"module"` // 模块 + IsEmpty bool `json:"isEmpty"` // 是否为空包 +} + +// PredesignedModuleInfo 预设计模块信息 +type PredesignedModuleInfo struct { + ModuleName string `json:"moduleName"` // 模块名称 + PackageName string `json:"packageName"` // 包名 + Template string `json:"template"` // 模板类型 + FilePaths []string `json:"filePaths"` // 文件路径列表 + Description string `json:"description"` // 描述 +} + +// CleanupInfo 清理信息 +type CleanupInfo struct { + DeletedPackages []string `json:"deletedPackages"` // 已删除的包 + DeletedModules []string `json:"deletedModules"` // 已删除的模块 + CleanupMessage string `json:"cleanupMessage"` // 清理消息 +} + +// New 创建GVA分析器工具 +func (g *GVAAnalyzer) New() mcp.Tool { + return mcp.NewTool("gva_analyze", + mcp.WithDescription("返回当前系统中有效的包和模块信息,并分析用户需求是否需要创建新的包、模块和字典。同时检查并清理空包,确保系统整洁。"), + mcp.WithString("requirement", + mcp.Description("用户需求描述,用于分析是否需要创建新的包和模块"), + mcp.Required(), + ), + ) +} + +// Handle 处理分析请求 +func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 解析请求参数 + requirementStr, ok := request.GetArguments()["requirement"].(string) + if !ok || requirementStr == "" { + return nil, errors.New("参数错误:requirement 必须是非空字符串") + } + + // 创建分析请求 + analyzeReq := AnalyzeRequest{ + Requirement: requirementStr, + } + + // 执行分析逻辑 + response, err := g.performAnalysis(ctx, analyzeReq) + if err != nil { + return nil, fmt.Errorf("分析失败: %v", err) + } + + // 序列化响应 + responseJSON, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("序列化响应失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseJSON)), + }, + }, nil +} + +// performAnalysis 执行分析逻辑 +func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) { + // 1. 获取数据库中的包信息 + var packages []model.SysAutoCodePackage + if err := global.GVA_DB.Find(&packages).Error; err != nil { + return nil, fmt.Errorf("获取包信息失败: %v", err) + } + + // 2. 获取历史记录 + var histories []model.SysAutoCodeHistory + if err := global.GVA_DB.Find(&histories).Error; err != nil { + return nil, fmt.Errorf("获取历史记录失败: %v", err) + } + + // 3. 检查空包并进行清理 + cleanupInfo := &CleanupInfo{ + DeletedPackages: []string{}, + DeletedModules: []string{}, + } + + var validPackages []model.SysAutoCodePackage + var emptyPackageHistoryIDs []uint + + for _, pkg := range packages { + isEmpty, err := g.isPackageFolderEmpty(pkg.PackageName, pkg.Template) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err)) + continue + } + + if isEmpty { + // 删除空包文件夹 + if err := g.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err)) + } else { + cleanupInfo.DeletedPackages = append(cleanupInfo.DeletedPackages, pkg.PackageName) + } + + // 删除数据库记录 + if err := global.GVA_DB.Delete(&pkg).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err)) + } + + // 收集相关的历史记录ID + for _, history := range histories { + if history.Package == pkg.PackageName { + emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID) + cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName) + } + } + } else { + validPackages = append(validPackages, pkg) + } + } + + // 5. 清理空包相关的历史记录和脏历史记录 + var dirtyHistoryIDs []uint + for _, history := range histories { + // 检查是否为空包相关的历史记录 + for _, emptyID := range emptyPackageHistoryIDs { + if history.ID == emptyID { + dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID) + break + } + } + } + + // 删除脏历史记录 + if len(dirtyHistoryIDs) > 0 { + if err := global.GVA_DB.Delete(&model.SysAutoCodeHistory{}, "id IN ?", dirtyHistoryIDs).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", len(dirtyHistoryIDs))) + } + + // 清理相关的API和菜单记录 + if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err)) + } + } + + // 6. 扫描预设计模块 + predesignedModules, err := g.scanPredesignedModules() + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err)) + predesignedModules = []PredesignedModuleInfo{} // 设置为空列表,不影响主流程 + } + + // 7. 过滤掉与已删除包相关的模块 + filteredModules := []PredesignedModuleInfo{} + for _, module := range predesignedModules { + isDeleted := false + for _, deletedPkg := range cleanupInfo.DeletedPackages { + if module.PackageName == deletedPkg { + isDeleted = true + break + } + } + if !isDeleted { + filteredModules = append(filteredModules, module) + } + } + + // 8. 构建分析结果消息 + var analysisMessage strings.Builder + if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 { + analysisMessage.WriteString("**系统清理完成**\n\n") + if len(cleanupInfo.DeletedPackages) > 0 { + analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", "))) + } + if len(cleanupInfo.DeletedModules) > 0 { + analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", "))) + } + analysisMessage.WriteString("\n") + cleanupInfo.CleanupMessage = analysisMessage.String() + } + + analysisMessage.WriteString(" **分析结果**\n\n") + analysisMessage.WriteString(fmt.Sprintf("- **现有包数量**: %d\n", len(validPackages))) + analysisMessage.WriteString(fmt.Sprintf("- **预设计模块数量**: %d\n\n", len(filteredModules))) + + // 9. 转换包信息 + existingPackages := make([]PackageInfo, len(validPackages)) + for i, pkg := range validPackages { + existingPackages[i] = PackageInfo{ + PackageName: pkg.PackageName, + Template: pkg.Template, + Label: pkg.Label, + Desc: pkg.Desc, + Module: pkg.Module, + IsEmpty: false, // 已经过滤掉空包 + } + } + + dictionaries := []DictionaryPre{} // 这里可以根据需要填充字典信息 + err = global.GVA_DB.Table("sys_dictionaries").Find(&dictionaries, "deleted_at is null").Error + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err)) + dictionaries = []DictionaryPre{} // 设置为空列表,不影响主流程 + } + + // 10. 构建响应 + response := &AnalyzeResponse{ + ExistingPackages: existingPackages, + PredesignedModules: filteredModules, + Dictionaries: dictionaries, + } + + return response, nil +} + +// isPackageFolderEmpty 检查包文件夹是否为空 +func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) { + // 根据模板类型确定基础路径 + var basePath string + if template == "plugin" { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + } else { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName) + } + + // 检查文件夹是否存在 + if _, err := os.Stat(basePath); os.IsNotExist(err) { + return true, nil // 文件夹不存在,视为空 + } else if err != nil { + return false, err // 其他错误 + } + + // 读取文件夹内容 + entries, err := os.ReadDir(basePath) + if err != nil { + return false, err + } + + // 检查是否有.go文件 + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { + return false, nil // 找到.go文件,不为空 + } + } + + return true, nil // 没有找到.go文件,为空 +} + +// removeEmptyPackageFolder 删除空包文件夹 +func (g *GVAAnalyzer) removeEmptyPackageFolder(packageName, template string) error { + var basePath string + if template == "plugin" { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + } else { + // 对于package类型,需要删除多个目录 + paths := []string{ + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName), + } + for _, path := range paths { + if err := g.removeDirectoryIfExists(path); err != nil { + return err + } + } + return nil + } + + return g.removeDirectoryIfExists(basePath) +} + +// removeDirectoryIfExists 删除目录(如果存在) +func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error { + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + return nil // 目录不存在,无需删除 + } else if err != nil { + return err // 其他错误 + } + + return os.RemoveAll(dirPath) +} + +// cleanupRelatedApiAndMenus 清理相关的API和菜单记录 +func (g *GVAAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error { + if len(historyIDs) == 0 { + return nil + } + + // 这里可以根据需要实现具体的API和菜单清理逻辑 + // 由于涉及到具体的业务逻辑,这里只做日志记录 + global.GVA_LOG.Info(fmt.Sprintf("清理历史记录ID %v 相关的API和菜单记录", historyIDs)) + + // 可以调用service层的相关方法进行清理 + // 例如:service.ServiceGroupApp.SystemApiService.DeleteApisByIds(historyIDs) + // 例如:service.ServiceGroupApp.MenuService.DeleteMenusByIds(historyIDs) + + return nil +} + +// scanPredesignedModules 扫描预设计模块 +func (g *GVAAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) { + // 获取autocode配置路径 + autocodeRoot := global.GVA_CONFIG.AutoCode.Root + if autocodeRoot == "" { + return nil, errors.New("autocode根路径未配置") + } + + var modules []PredesignedModuleInfo + + // 扫描plugin目录 + pluginModules, err := g.scanPluginModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "plugin")) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描plugin模块失败: %v", err)) + } else { + modules = append(modules, pluginModules...) + } + + // 扫描model目录 + modelModules, err := g.scanModelModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "model")) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描model模块失败: %v", err)) + } else { + modules = append(modules, modelModules...) + } + + return modules, nil +} + +// scanPluginModules 扫描插件模块 +func (g *GVAAnalyzer) scanPluginModules(pluginDir string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(pluginDir); os.IsNotExist(err) { + return modules, nil // 目录不存在,返回空列表 + } + + entries, err := os.ReadDir(pluginDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsDir() { + pluginName := entry.Name() + pluginPath := filepath.Join(pluginDir, pluginName) + + // 查找model目录 + modelDir := filepath.Join(pluginPath, "model") + if _, err := os.Stat(modelDir); err == nil { + // 扫描model目录下的模块 + pluginModules, err := g.scanModulesInDirectory(modelDir, pluginName, "plugin") + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描插件 %s 的模块失败: %v", pluginName, err)) + continue + } + modules = append(modules, pluginModules...) + } + } + } + + return modules, nil +} + +// scanModelModules 扫描模型模块 +func (g *GVAAnalyzer) scanModelModules(modelDir string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(modelDir); os.IsNotExist(err) { + return modules, nil // 目录不存在,返回空列表 + } + + entries, err := os.ReadDir(modelDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsDir() { + packageName := entry.Name() + packagePath := filepath.Join(modelDir, packageName) + + // 扫描包目录下的模块 + packageModules, err := g.scanModulesInDirectory(packagePath, packageName, "package") + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描包 %s 的模块失败: %v", packageName, err)) + continue + } + modules = append(modules, packageModules...) + } + } + + return modules, nil +} + +// scanModulesInDirectory 扫描目录中的模块 +func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { + moduleName := strings.TrimSuffix(entry.Name(), ".go") + filePath := filepath.Join(dir, entry.Name()) + + module := PredesignedModuleInfo{ + ModuleName: moduleName, + PackageName: packageName, + Template: template, + FilePaths: []string{filePath}, + Description: fmt.Sprintf("%s模块中的%s", packageName, moduleName), + } + modules = append(modules, module) + } + } + + return modules, nil +} diff --git a/server/mcp/gva_auto_generate.go b/server/mcp/gva_auto_generate.go deleted file mode 100644 index 8fe4ef85d7..0000000000 --- a/server/mcp/gva_auto_generate.go +++ /dev/null @@ -1,1755 +0,0 @@ -package mcpTool - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "time" - "unicode" - - "github.com/flipped-aurora/gin-vue-admin/server/global" - common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" - model "github.com/flipped-aurora/gin-vue-admin/server/model/system" - "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" - "github.com/flipped-aurora/gin-vue-admin/server/service" - systemService "github.com/flipped-aurora/gin-vue-admin/server/service/system" - "github.com/mark3labs/mcp-go/mcp" - "gorm.io/gorm" -) - -func init() { - RegisterTool(&AutomationModuleAnalyzer{}) -} - -type AutomationModuleAnalyzer struct{} - -// ModuleInfo 模块信息 -type ModuleInfo struct { - ID uint `json:"id"` - PackageName string `json:"packageName"` - Label string `json:"label"` - Desc string `json:"desc"` - Template string `json:"template"` // "plugin" 或 "package" - Module string `json:"module"` -} - -// HistoryInfo 历史记录信息 -type HistoryInfo struct { - ID uint `json:"id"` - StructName string `json:"structName"` - TableName string `json:"tableName"` - PackageName string `json:"packageName"` - BusinessDB string `json:"businessDB"` - Description string `json:"description"` - Abbreviation string `json:"abbreviation"` - CreatedAt string `json:"createdAt"` -} - -// PredesignedModuleInfo 预设计模块信息 -type PredesignedModuleInfo struct { - PackageName string `json:"packageName"` - PackageType string `json:"packageType"` // "plugin" 或 "package" - ModuleName string `json:"moduleName"` - Path string `json:"path"` - Modules []string `json:"modules"` // 包含的模块列表(如api、model、service等) - Description string `json:"description"` - StructName string `json:"structName,omitempty"` // 主要结构体名称 -} - -// AnalysisResponse 分析响应 -type AnalysisResponse struct { - Packages []ModuleInfo `json:"packages"` - History []HistoryInfo `json:"history"` - PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` - Message string `json:"message"` -} - -// ExecutionPlan 执行计划 - 支持批量创建 -type ExecutionPlan struct { - PackageName string `json:"packageName"` - PackageType string `json:"packageType"` // "plugin" 或 "package" - NeedCreatedPackage bool `json:"needCreatedPackage"` - NeedCreatedModules bool `json:"needCreatedModules"` - PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` - ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 改为数组支持多个模块 - Paths map[string]string `json:"paths,omitempty"` -} - -// ExecutionResult 执行结果 -type ExecutionResult struct { - Success bool `json:"success"` - Message string `json:"message"` - PackageID uint `json:"packageId,omitempty"` - HistoryID uint `json:"historyId,omitempty"` - Paths map[string]string `json:"paths,omitempty"` - NextActions []string `json:"nextActions,omitempty"` -} - -// ConfirmationRequest 确认请求结构 -type ConfirmationRequest struct { - PackageName string `json:"packageName"` - ModuleName string `json:"moduleName"` - NeedCreatedPackage bool `json:"needCreatedPackage"` - NeedCreatedModules bool `json:"needCreatedModules"` - PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` - ModulesInfo *request.AutoCode `json:"modulesInfo,omitempty"` -} - -// ConfirmationResponse 确认响应结构 -type ConfirmationResponse struct { - Message string `json:"message"` - PackageConfirm bool `json:"packageConfirm"` - ModulesConfirm bool `json:"modulesConfirm"` - CanProceed bool `json:"canProceed"` - ConfirmationKey string `json:"confirmationKey"` -} - -// New 返回工具注册信息 -func (t *AutomationModuleAnalyzer) New() mcp.Tool { - return mcp.NewTool("gva_auto_generate", - mcp.WithDescription(`**🔧 核心执行工具:接收requirement_analyzer分析结果,执行具体的模块创建操作** - -**工作流位置:** -- **第二优先级**:在requirement_analyzer之后使用 -- **接收输入**:来自requirement_analyzer的1xxx2xxx格式分析结果 -- **执行操作**:根据分析结果创建完整模块、包、功能模块 - -**批量创建功能:** -- 支持在单个ExecutionPlan中创建多个模块 -- modulesInfo字段为数组,可包含多个模块配置 -- 一次性处理多个模块的创建和字典生成 -- 与requirement_analyzer配合实现完整工作流 - -分步骤分析自动化模块:1) 分析现有模块信息供AI选择 2) 请求用户确认 3) 根据确认结果执行创建操作 - -**新功能:自动字典创建** -- 当结构体字段使用了字典类型(dictType不为空)时,系统会自动检查字典是否存在 -- 如果字典不存在,会自动创建对应的字典及默认的字典详情项 -- 字典创建包括:字典主表记录和默认的选项值(选项1、选项2等) - -**推荐工作流:** -1. 用户提出需求 → requirement_analyzer(最高优先级) -2. AI分析需求为1xxx2xxx格式 → gva_auto_generate(执行创建) -3. 创建完成后,根据需要使用其他辅助工具 - -**重要限制:** -- 当needCreatedModules=true时,模块创建会自动生成API和菜单,因此不应再调用api_creator和menu_creator工具 -- 只有在单独创建API或菜单(不涉及模块创建)时才使用api_creator和menu_creator工具 - -重要:ExecutionPlan结构体格式要求(支持批量创建): -{ - "packageName": "包名(string)", - "packageType": "package或plugin(string)", - "needCreatedPackage": "是否需要创建包(bool)", - "needCreatedModules": "是否需要创建模块(bool)", - "packageInfo": { - "desc": "描述(string)", - "label": "展示名(string)", - "template": "package或plugin(string)", - "packageName": "包名(string)" - }, - "modulesInfo": [{ - "package": "包名(string)", - "tableName": "数据库表名(string)", - "businessDB": "业务数据库(string)", - "structName": "结构体名(string)", - "packageName": "文件名称(string)", - "description": "中文描述(string)", - "abbreviation": "简称(string)", - "humpPackageName": "文件名称 一般是结构体名的小驼峰(string)", - "gvaModel": "是否使用GVA模型(bool) 固定为true 后续不需要创建ID created_at deleted_at updated_at", - "autoMigrate": "是否自动迁移(bool)", - "autoCreateResource": "是否创建资源(bool)", - "autoCreateApiToSql": "是否创建API(bool)", - "autoCreateMenuToSql": "是否创建菜单(bool)", - "autoCreateBtnAuth": "是否创建按钮权限(bool)", - "onlyTemplate": "是否仅模板(bool)", - "isTree": "是否树形结构(bool)", - "treeJson": "树形JSON字段(string)", - "isAdd": "是否新增(bool) 固定为false", - "generateWeb": "是否生成前端(bool)", - "generateServer": "是否生成后端(bool)", - "fields": [{ - "fieldName": "字段名(string)必须大写开头", - "fieldDesc": "字段描述(string)", - "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)", - "fieldJson": "JSON标签(string 必须是小驼峰命名,例:userName)", - "dataTypeLong": "数据长度(string)", - "comment": "注释(string)", - "columnName": "数据库列名(string)", - "fieldSearchType": "搜索类型:=/>/=/<=/NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN等(string)", - "fieldSearchHide": "是否隐藏搜索(bool)", - "dictType": "字典类型(string)", - "form": "表单显示(bool)", - "table": "表格显示(bool)", - "desc": "详情显示(bool)", - "excel": "导入导出(bool)", - "require": "是否必填(bool)", - "defaultValue": "默认值(string),JSON类型(array,json,file,pictures)请保持为空他们不可以设置默认值", - "errorText": "错误提示(string)", - "clearable": "是否可清空(bool)", - "sort": "是否排序(bool)", - "primaryKey": "是否主键(bool)", - "dataSource": "数据源配置(object) - 用于配置字段的关联表信息,结构:{\"dbName\":\"数据库名\",\"table\":\"关联表名\",\"label\":\"显示字段\",\"value\":\"值字段\",\"association\":1或2(1=一对一,2=一对多),\"hasDeletedAt\":true/false}。\n\n**获取表名提示:**\n- 可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名\n- 例如:SysUser 的表名为 \"sys_users\",ExaFileUploadAndDownload 的表名为 \"exa_file_upload_and_downloads\"\n- 插件模块示例:Info 的表名为 \"gva_announcements_info\"\n\n**获取数据库名提示:**\n- 主数据库:通常使用 \"gva\"(默认数据库标识)\n- 多数据库:可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段\n- 如果用户未提及关联多数据库信息 则使用默认数据库 默认数据库的情况下 dbName此处填写为空", - "checkDataSource": "是否检查数据源(bool) - 启用后会验证关联表的存在性", - "fieldIndexType": "索引类型(string)" - }] - }, { - "package": "包名(string)", - "tableName": "第二个模块的表名(string)", - "structName": "第二个模块的结构体名(string)", - "description": "第二个模块的描述(string)", - "...": "更多模块配置..." - }] -} - -注意: -1. needCreatedPackage=true时packageInfo必需 -2. needCreatedModules=true时modulesInfo必需 -3. packageType只能是"package"或"plugin" -4. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组) -5. 搜索类型支持:=,!=,>,>=,<,<=,NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN -6. gvaModel=true时自动包含ID,CreatedAt,UpdatedAt,DeletedAt字段 -7. **重要**:当gvaModel=false时,必须有一个字段的primaryKey=true,否则会导致PrimaryField为nil错误 -8. **重要**:当gvaModel=true时,系统会自动设置ID字段为主键,无需手动设置primaryKey=true -9. 智能字典创建功能:当字段使用字典类型(DictType)时,系统会: - - 自动检查字典是否存在,如果不存在则创建字典 - - 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景 - - 为无法识别的字典类型提供通用默认选项 -10. **模块关联配置**:当需要配置模块间的关联关系时,使用dataSource字段: - - **dbName**: 关联的数据库名称 - - **table**: 关联的表名 - - **label**: 用于显示的字段名(如name、title等) - - **value**: 用于存储的值字段名(通常是id) - - **association**: 关联关系类型(1=一对一关联,2=一对多关联) - - **hasDeletedAt**: 关联表是否有软删除字段 - - **checkDataSource**: 设为true时会验证关联表的存在性 - - 示例:{"dbName":"gva","table":"sys_users","label":"username","value":"id","association":2,"hasDeletedAt":true}`), - mcp.WithString("action", - mcp.Required(), - mcp.Description("执行操作:'analyze' 分析现有模块信息,'confirm' 请求用户确认创建,'execute' 执行创建操作(支持批量创建多个模块)"), - ), - mcp.WithString("requirement", - mcp.Description("用户需求描述(action=analyze时必需)"), - ), - mcp.WithObject("executionPlan", - mcp.Description("执行计划(action=confirm或execute时必需,必须严格按照上述格式提供完整的JSON对象)"), - ), - mcp.WithString("packageConfirm", - mcp.Description("用户对创建包的确认(action=execute时,如果需要创建包则必需):'yes' 或 'no'"), - ), - mcp.WithString("modulesConfirm", - mcp.Description("用户对创建模块的确认(action=execute时,如果需要创建模块则必需):'yes' 或 'no'"), - ), - ) -} - -// scanPredesignedModules 扫描预设计的模块 -func (t *AutomationModuleAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) { - var predesignedModules []PredesignedModuleInfo - - // 获取autocode配置路径 - if global.GVA_CONFIG.AutoCode.Root == "" { - return predesignedModules, nil // 配置不存在时返回空列表,不报错 - } - - serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) - - // 扫描plugin目录下的各个插件模块 - pluginPath := filepath.Join(serverPath, "plugin") - if pluginModules, err := t.scanPluginModules(pluginPath); err == nil { - predesignedModules = append(predesignedModules, pluginModules...) - } - - // 扫描model目录下的各个包模块 - modelPath := filepath.Join(serverPath, "model") - if packageModules, err := t.scanPackageModules(modelPath); err == nil { - predesignedModules = append(predesignedModules, packageModules...) - } - - return predesignedModules, nil -} - -// scanPluginModules 扫描plugin目录下的各个插件模块 -func (t *AutomationModuleAnalyzer) scanPluginModules(pluginPath string) ([]PredesignedModuleInfo, error) { - var modules []PredesignedModuleInfo - - if _, err := os.Stat(pluginPath); os.IsNotExist(err) { - return modules, nil - } - - entries, err := os.ReadDir(pluginPath) - if err != nil { - return modules, err - } - - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - pluginName := entry.Name() - pluginDir := filepath.Join(pluginPath, pluginName) - - // 扫描插件下的model目录,查找具体的模块文件 - modelDir := filepath.Join(pluginDir, "model") - if _, err := os.Stat(modelDir); err == nil { - if pluginModules, err := t.scanModuleFiles(modelDir, pluginName, "plugin"); err == nil { - modules = append(modules, pluginModules...) - } - } - } - - return modules, nil -} - -// scanPackageModules 扫描model目录下的各个包模块 -func (t *AutomationModuleAnalyzer) scanPackageModules(modelPath string) ([]PredesignedModuleInfo, error) { - var modules []PredesignedModuleInfo - - if _, err := os.Stat(modelPath); os.IsNotExist(err) { - return modules, nil - } - - entries, err := os.ReadDir(modelPath) - if err != nil { - return modules, err - } - - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - packageName := entry.Name() - // 跳过一些系统目录 - if packageName == "common" || packageName == "request" || packageName == "response" { - continue - } - - packageDir := filepath.Join(modelPath, packageName) - - // 扫描包目录下的模块文件 - if packageModules, err := t.scanModuleFiles(packageDir, packageName, "package"); err == nil { - modules = append(modules, packageModules...) - } - } - - return modules, nil -} - -// scanModuleFiles 扫描目录下的Go文件,识别具体的模块 -func (t *AutomationModuleAnalyzer) scanModuleFiles(dir, packageName, packageType string) ([]PredesignedModuleInfo, error) { - var modules []PredesignedModuleInfo - - entries, err := os.ReadDir(dir) - if err != nil { - return modules, err - } - - for _, entry := range entries { - if entry.IsDir() { - continue - } - - fileName := entry.Name() - if !strings.HasSuffix(fileName, ".go") { - continue - } - - // 跳过一些非模块文件 - if strings.HasSuffix(fileName, "_test.go") || - fileName == "enter.go" || - fileName == "request.go" || - fileName == "response.go" { - continue - } - - filePath := filepath.Join(dir, fileName) - moduleName := strings.TrimSuffix(fileName, ".go") - - // 分析模块文件,提取结构体信息 - if moduleInfo, err := t.analyzeModuleFile(filePath, packageName, moduleName, packageType); err == nil { - modules = append(modules, *moduleInfo) - } - } - - return modules, nil -} - -// analyzeModuleFile 分析具体的模块文件 -func (t *AutomationModuleAnalyzer) analyzeModuleFile(filePath, packageName, moduleName, packageType string) (*PredesignedModuleInfo, error) { - content, err := os.ReadFile(filePath) - if err != nil { - return nil, err - } - - fileContent := string(content) - - // 提取结构体名称和描述 - structNames := t.extractStructNames(fileContent) - description := t.extractModuleDescription(fileContent, moduleName) - - // 确定主要结构体名称 - mainStruct := moduleName - if len(structNames) > 0 { - // 优先选择与文件名相关的结构体 - for _, structName := range structNames { - if strings.Contains(strings.ToLower(structName), strings.ToLower(moduleName)) { - mainStruct = structName - break - } - } - if mainStruct == moduleName && len(structNames) > 0 { - mainStruct = structNames[0] // 如果没有匹配的,使用第一个 - } - } - - return &PredesignedModuleInfo{ - PackageName: packageName, - PackageType: packageType, - ModuleName: moduleName, - Path: filePath, - Modules: structNames, - Description: description, - StructName: mainStruct, - }, nil -} - -// extractStructNames 从文件内容中提取结构体名称 -func (t *AutomationModuleAnalyzer) extractStructNames(content string) []string { - var structNames []string - lines := strings.Split(content, "\n") - - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "type ") && strings.Contains(line, " struct") { - // 提取结构体名称 - parts := strings.Fields(line) - if len(parts) >= 3 && parts[2] == "struct" { - structNames = append(structNames, parts[1]) - } - } - } - - return structNames -} - -// extractModuleDescription 从文件内容中提取模块描述 -func (t *AutomationModuleAnalyzer) extractModuleDescription(content, moduleName string) string { - lines := strings.Split(content, "\n") - - // 查找package注释 - for i, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "package ") { - // 向上查找注释 - for j := i - 1; j >= 0; j-- { - commentLine := strings.TrimSpace(lines[j]) - if strings.HasPrefix(commentLine, "//") { - comment := strings.TrimSpace(strings.TrimPrefix(commentLine, "//")) - if comment != "" && len(comment) > 5 { - return comment - } - } else if commentLine != "" { - break - } - } - break - } - } - - // 查找结构体注释 - for i, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "type ") && strings.Contains(line, " struct") { - // 向上查找注释 - for j := i - 1; j >= 0; j-- { - commentLine := strings.TrimSpace(lines[j]) - if strings.HasPrefix(commentLine, "//") { - comment := strings.TrimSpace(strings.TrimPrefix(commentLine, "//")) - if comment != "" && len(comment) > 5 { - return comment - } - } else if commentLine != "" { - break - } - } - break - } - } - - return fmt.Sprintf("预设计的模块:%s", moduleName) -} - -// Handle 处理工具调用 -func (t *AutomationModuleAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - action, ok := request.GetArguments()["action"].(string) - if !ok || action == "" { - return nil, errors.New("参数错误:action 必须是非空字符串") - } - - switch action { - case "analyze": - return t.handleAnalyze(ctx, request) - case "confirm": - return t.handleConfirm(ctx, request) - case "execute": - return t.handleExecute(ctx, request) - default: - return nil, errors.New("无效的操作:action 必须是 'analyze'、'confirm' 或 'execute'") - } -} - -// handleAnalyze 处理分析请求 -func (t *AutomationModuleAnalyzer) handleAnalyze(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - requirement, ok := request.GetArguments()["requirement"].(string) - if !ok || requirement == "" { - return nil, errors.New("参数错误:requirement 必须是非空字符串") - } - - // 检测用户是否想要创建插件 - suggestedType, isPlugin, confidence := t.detectPluginIntent(requirement) - pluginDetectionMsg := "" - if isPlugin { - pluginDetectionMsg = fmt.Sprintf("\n\n🔍 **插件检测结果**:检测到用户想要创建插件(置信度:%s)\n⚠️ **重要提醒**:当用户提到插件时,packageType和template字段都必须设置为 \"plugin\",不能使用 \"package\"!", confidence) - } else { - pluginDetectionMsg = fmt.Sprintf("\n\n🔍 **类型检测结果**:建议使用 %s 类型", suggestedType) - } - - // 从数据库获取所有自动化包信息 - var packages []model.SysAutoCodePackage - if err := global.GVA_DB.Find(&packages).Error; err != nil { - return nil, fmt.Errorf("获取包信息失败: %v", err) - } - - // 从数据库获取所有历史记录 - var histories []model.SysAutoCodeHistory - if err := global.GVA_DB.Find(&histories).Error; err != nil { - return nil, fmt.Errorf("获取历史记录失败: %v", err) - } - - // 转换包信息并检查空文件夹 - var moduleInfos []ModuleInfo - var validPackages []model.SysAutoCodePackage - var emptyPackageIDs []uint - var emptyPackageNames []string - - for _, pkg := range packages { - // 检查包对应的文件夹是否为空 - isEmpty, err := t.isPackageFolderEmpty(pkg.PackageName, pkg.Template) - if err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 文件夹失败: %v", pkg.PackageName, err)) - // 如果检查失败,仍然保留该包 - validPackages = append(validPackages, pkg) - continue - } - - if isEmpty { - // 记录需要删除的包ID和包名 - emptyPackageIDs = append(emptyPackageIDs, pkg.ID) - emptyPackageNames = append(emptyPackageNames, pkg.PackageName) - global.GVA_LOG.Info(fmt.Sprintf("发现空包文件夹: %s,将删除数据库记录和文件夹", pkg.PackageName)) - - // 删除空文件夹 - if err := t.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err)) - } - } else { - // 文件夹不为空,保留该包 - validPackages = append(validPackages, pkg) - } - } - - // 批量删除空包的数据库记录 - if len(emptyPackageIDs) > 0 { - if err := global.GVA_DB.Where("id IN ?", emptyPackageIDs).Delete(&model.SysAutoCodePackage{}).Error; err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("删除空包数据库记录失败: %v", err)) - } else { - global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个空包的数据库记录", len(emptyPackageIDs))) - } - } - - // 转换有效的包信息 - for _, pkg := range validPackages { - moduleInfos = append(moduleInfos, ModuleInfo{ - ID: pkg.ID, - PackageName: pkg.PackageName, - Label: pkg.Label, - Desc: pkg.Desc, - Template: pkg.Template, - Module: pkg.Module, - }) - } - - // 删除与空包相关的历史记录 - var emptyHistoryIDs []uint - if len(emptyPackageNames) > 0 { - for _, history := range histories { - for _, emptyPackageName := range emptyPackageNames { - if history.Package == emptyPackageName { - emptyHistoryIDs = append(emptyHistoryIDs, history.ID) - break - } - } - } - - // 清理相关的API和菜单记录 - if len(emptyHistoryIDs) > 0 { - if err := t.cleanupRelatedApiAndMenus(emptyHistoryIDs); err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("清理空包相关API和菜单失败: %v", err)) - } - } - - // 批量删除相关历史记录 - if len(emptyHistoryIDs) > 0 { - if err := global.GVA_DB.Where("id IN ?", emptyHistoryIDs).Delete(&model.SysAutoCodeHistory{}).Error; err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("删除空包相关历史记录失败: %v", err)) - } else { - global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个空包相关的历史记录", len(emptyHistoryIDs))) - } - } - } - - // 创建有效包名的映射,用于快速查找 - validPackageNames := make(map[string]bool) - for _, pkg := range validPackages { - validPackageNames[pkg.PackageName] = true - } - - // 收集需要删除的脏历史记录ID(包名不在有效包列表中的历史记录) - var dirtyHistoryIDs []uint - for _, history := range histories { - if !validPackageNames[history.Package] { - dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID) - } - } - - // 删除脏历史记录 - if len(dirtyHistoryIDs) > 0 { - // 清理相关的API和菜单记录 - if err := t.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("清理脏历史记录相关API和菜单失败: %v", err)) - } - - if err := global.GVA_DB.Where("id IN ?", dirtyHistoryIDs).Delete(&model.SysAutoCodeHistory{}).Error; err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err)) - } else { - global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个脏历史记录(包名不在有效包列表中)", len(dirtyHistoryIDs))) - } - } - - // 转换有效的历史记录(只保留包名存在于有效包列表中的历史记录) - var historyInfos []HistoryInfo - for _, history := range histories { - // 只保留包名存在于有效包列表中的历史记录 - if validPackageNames[history.Package] { - historyInfos = append(historyInfos, HistoryInfo{ - ID: history.ID, - StructName: history.StructName, - TableName: history.TableName(), - PackageName: history.Package, - BusinessDB: history.BusinessDB, - Description: history.Description, - Abbreviation: history.Abbreviation, - CreatedAt: history.CreatedAt.Format("2006-01-02 15:04:05"), - }) - } - } - - // 扫描预设计的模块 - allPredesignedModules, err := t.scanPredesignedModules() - if err != nil { - global.GVA_LOG.Warn("扫描预设计模块失败" + err.Error()) - allPredesignedModules = []PredesignedModuleInfo{} // 确保不为nil - } - - // 过滤掉与已删除包相关的预设计模块 - var predesignedModules []PredesignedModuleInfo - for _, module := range allPredesignedModules { - isDeleted := false - for _, emptyPackageName := range emptyPackageNames { - if module.PackageName == emptyPackageName { - isDeleted = true - break - } - } - - // 只保留未被删除包的预设计模块 - if !isDeleted { - predesignedModules = append(predesignedModules, module) - } - } - - // 构建分析结果消息 - var message string - var deletionDetails []string - - // 收集删除信息 - if len(emptyHistoryIDs) > 0 { - deletionDetails = append(deletionDetails, fmt.Sprintf("%d个空包相关历史记录", len(emptyHistoryIDs))) - } - if len(dirtyHistoryIDs) > 0 { - deletionDetails = append(deletionDetails, fmt.Sprintf("%d个脏历史记录", len(dirtyHistoryIDs))) - } - if len(allPredesignedModules) > len(predesignedModules) { - deletionDetails = append(deletionDetails, fmt.Sprintf("%d个相关预设计模块", len(allPredesignedModules)-len(predesignedModules))) - } - - if len(emptyPackageNames) > 0 || len(deletionDetails) > 0 { - var cleanupInfo string - if len(emptyPackageNames) > 0 { - cleanupInfo = fmt.Sprintf("检测到存在 %s 包但内容为空,我已经删除这些包的文件夹(包括model、api、service、router目录)和数据库记录", strings.Join(emptyPackageNames, "、")) - } - - deletionInfo := "" - if len(deletionDetails) > 0 { - if cleanupInfo != "" { - deletionInfo = fmt.Sprintf(",同时删除了%s", strings.Join(deletionDetails, "、")) - } else { - deletionInfo = fmt.Sprintf("检测到脏数据,已删除%s", strings.Join(deletionDetails, "、")) - } - } - - if cleanupInfo != "" { - message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块。%s%s,如果需要使用这些包名,需要重新创建。请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules), cleanupInfo, deletionInfo) - } else { - message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块。%s。请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules), deletionInfo) - } - } else { - message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块,请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules)) - } - - // 构建分析结果 - analysisResult := AnalysisResponse{ - Packages: moduleInfos, - History: historyInfos, - PredesignedModules: predesignedModules, - Message: message, - } - - resultJSON, err := json.MarshalIndent(analysisResult, "", " ") - if err != nil { - return nil, fmt.Errorf("序列化结果失败: %v", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf(`分析结果: - -%s - -请AI根据用户需求:%s%s - -%s - -分析现有的包、历史记录和预设计模块,然后构建ExecutionPlan结构体调用execute操作。 - -**预设计模块说明**: -- 预设计模块是已经存在于autocode路径下的package或plugin -- 这些模块包含了预先设计好的代码结构,可以直接使用或作为参考 -- 如果用户需求与某个预设计模块匹配,可以考虑直接使用该模块或基于它进行扩展 - -**字典选项生成说明**: -- 当字段需要使用字典类型时(dictType不为空),请使用 generate_dictionary_options 工具 -- 该工具允许AI根据字段描述智能生成合适的字典选项 -- 调用示例: - { - "dictType": "user_status", - "fieldDesc": "用户状态", - "options": [ - {"label": "正常", "value": "1", "sort": 1}, - {"label": "禁用", "value": "0", "sort": 2} - ], - "dictName": "用户状态字典", - "description": "用于管理用户账户状态的字典" - } -- 请在创建模块之前先创建所需的字典选项 - -重要提醒:ExecutionPlan必须严格按照以下格式(支持批量创建多个模块): -{ - "packageName": "包名", - "packageType": "package或plugin", // 当用户提到插件时必须是"plugin" - "needCreatedPackage": true/false, - "needCreatedModules": true/false, - "packageInfo": { - "desc": "描述", - "label": "展示名", - "template": "package或plugin", // 必须与packageType保持一致! - "packageName": "包名" - }, - "modulesInfo": [{ - "package": "包名", - "tableName": "数据库表名", - "businessDB": "", - "structName": "结构体名", - "packageName": "文件名称小驼峰模式 一般是结构体名的小驼峰", - "description": "中文描述", - "abbreviation": "简称 package和结构体简称不可同名 小驼峰模式", - "humpPackageName": "一般是结构体名的下划线分割的小驼峰 例如:sys_user", - "gvaModel": true, - "autoMigrate": true, - "autoCreateResource": true/false 用户不特地强调开启资源标识则为false, - "autoCreateApiToSql": true, - "autoCreateMenuToSql": true, - "autoCreateBtnAuth": false/true 用户不特地强调创建按钮权限则为false, - "onlyTemplate": false, - "isTree": false, - "treeJson": "", - "isAdd": false, - "generateWeb": true, - "generateServer": true, - "fields": [{ - "fieldName": "字段名(必须大写开头)", - "fieldDesc": "字段描述", - "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)", - "fieldJson": "json标签(string 必须是小驼峰命名,例:userName)", - "dataTypeLong": "长度", - "comment": "注释", - "columnName": "数据库列名", - "fieldSearchType": "=/!=/>/=/<=/LIKE等 可以为空", - "fieldSearchHide": true/false, - "dictType": "", - "form": true/false 是否前端创建输入, - "table": true/false 是否前端表格展示, - "desc": true/false 是否前端详情展示, - "excel": true/false 是否导出Excel, - "require": true/false 是否必填, - "defaultValue": "", - "errorText": "错误提示", - "clearable": true, - "sort": false, - "primaryKey": "当gvaModel=false时必须有一个字段设为true(bool)", - "dataSource": null, - "checkDataSource": false, - "fieldIndexType": "" - }] - }, { - "package": "包名", - "tableName": "第二个模块的表名", - "structName": "第二个模块的结构体名", - "description": "第二个模块的描述", - "...": "更多模块配置..." - }] -} - -**重要提醒**:ExecutionPlan必须严格按照以下格式和验证规则: - -**插件类型检测规则(最重要)**: -1. 当用户需求中包含"插件"、"plugin"等关键词时,packageType和template都必须设置为"plugin" -2. packageType和template字段必须保持一致,不能一个是"package"另一个是"plugin" -3. 如果检测到插件意图但设置错误,会导致创建失败 - -**字段完整性要求**: -4. 所有字符串字段都不能为空(包括packageName、moduleName、structName、tableName、description等) -5. 所有布尔字段必须明确设置true或false,不能使用默认值 - -**主键设置规则(关键)**: -6. 当gvaModel=false时:fields数组中必须有且仅有一个字段的primaryKey=true -7. 当gvaModel=true时:系统自动创建ID主键,fields中所有字段的primaryKey都应为false -8. 主键设置错误会导致模板执行时PrimaryField为nil的严重错误! - -**包和模块创建逻辑**: -9. 如果存在可用的package,needCreatedPackage应设为false -10. 如果存在可用的modules,needCreatedModules应设为false -11. 如果发现合适的预设计模块,可以考虑基于它进行扩展而不是从零创建 - -**字典创建流程**: -12. 如果字段需要字典类型,请先使用 generate_dictionary_options 工具创建字典 -13. 字典创建成功后,再执行模块创建操作 - -`, string(resultJSON), requirement, pluginDetectionMsg, - func() string { - if len(emptyPackageNames) > 0 { - return fmt.Sprintf("**重要提醒**:检测到 %s 包存在但内容为空,已自动删除相关文件夹和数据库记录。如果用户需求涉及这些包名,请设置 needCreatedPackage=true 重新创建。", strings.Join(emptyPackageNames, "、")) - } - return "" - }()), - }, - }, - }, nil -} - -// handleConfirm 处理确认请求 -func (t *AutomationModuleAnalyzer) handleConfirm(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - executionPlanData, ok := request.GetArguments()["executionPlan"] - if !ok { - return nil, errors.New("参数错误:executionPlan 必须提供") - } - - // 解析执行计划 - planJSON, err := json.Marshal(executionPlanData) - if err != nil { - return nil, fmt.Errorf("解析执行计划失败: %v", err) - } - - var plan ExecutionPlan - err = json.Unmarshal(planJSON, &plan) - if err != nil { - return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) - } - - // 验证执行计划的完整性 - if err := t.validateExecutionPlan(&plan); err != nil { - return nil, fmt.Errorf("执行计划验证失败: %v", err) - } - - // 构建确认响应 - var moduleNames []string - for _, moduleInfo := range plan.ModulesInfo { - moduleNames = append(moduleNames, moduleInfo.StructName) - } - moduleNamesStr := strings.Join(moduleNames, "_") - - confirmResponse := ConfirmationResponse{ - Message: "请确认以下创建计划:", - PackageConfirm: plan.NeedCreatedPackage, - ModulesConfirm: plan.NeedCreatedModules, - CanProceed: true, - ConfirmationKey: fmt.Sprintf("%s_%s_%d", plan.PackageName, moduleNamesStr, time.Now().Unix()), - } - - // 构建详细的确认信息 - var confirmDetails strings.Builder - confirmDetails.WriteString(fmt.Sprintf("包名: %s\n", plan.PackageName)) - confirmDetails.WriteString(fmt.Sprintf("包类型: %s\n", plan.PackageType)) - - if plan.NeedCreatedPackage && plan.PackageInfo != nil { - confirmDetails.WriteString("\n需要创建包:\n") - confirmDetails.WriteString(fmt.Sprintf(" - 包名: %s\n", plan.PackageInfo.PackageName)) - confirmDetails.WriteString(fmt.Sprintf(" - 标签: %s\n", plan.PackageInfo.Label)) - confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", plan.PackageInfo.Desc)) - confirmDetails.WriteString(fmt.Sprintf(" - 模板: %s\n", plan.PackageInfo.Template)) - } - - if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { - confirmDetails.WriteString(fmt.Sprintf("\n需要创建模块 (共%d个):\n", len(plan.ModulesInfo))) - for i, moduleInfo := range plan.ModulesInfo { - confirmDetails.WriteString(fmt.Sprintf("\n模块 %d:\n", i+1)) - confirmDetails.WriteString(fmt.Sprintf(" - 结构体名: %s\n", moduleInfo.StructName)) - confirmDetails.WriteString(fmt.Sprintf(" - 表名: %s\n", moduleInfo.TableName)) - confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", moduleInfo.Description)) - confirmDetails.WriteString(fmt.Sprintf(" - 字段数量: %d\n", len(moduleInfo.Fields))) - confirmDetails.WriteString(" - 字段列表:\n") - for _, field := range moduleInfo.Fields { - confirmDetails.WriteString(fmt.Sprintf(" * %s (%s): %s\n", field.FieldName, field.FieldType, field.FieldDesc)) - } - } - } - - resultJSON, err := json.MarshalIndent(confirmResponse, "", " ") - if err != nil { - return nil, fmt.Errorf("序列化结果失败: %v", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("确认信息:\n\n%s\n\n详细信息:\n%s\n\n请用户确认是否继续执行此计划。如果确认,请使用execute操作并提供相应的确认参数。", string(resultJSON), confirmDetails.String()), - }, - }, - }, nil -} - -// handleExecute 处理执行请求 -func (t *AutomationModuleAnalyzer) handleExecute(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - executionPlanData, ok := request.GetArguments()["executionPlan"] - if !ok { - return nil, errors.New("参数错误:executionPlan 必须提供") - } - - // 解析执行计划 - planJSON, err := json.Marshal(executionPlanData) - if err != nil { - return nil, fmt.Errorf("解析执行计划失败: %v", err) - } - - var plan ExecutionPlan - err = json.Unmarshal(planJSON, &plan) - if err != nil { - return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) - } - - // 验证执行计划的完整性 - if err := t.validateExecutionPlan(&plan); err != nil { - return nil, fmt.Errorf("执行计划验证失败: %v", err) - } - - // 检查用户确认 - if plan.NeedCreatedPackage { - packageConfirm, ok := request.GetArguments()["packageConfirm"].(string) - if !ok || (packageConfirm != "yes" && packageConfirm != "no") { - return nil, errors.New("参数错误:当需要创建包时,packageConfirm 必须是 'yes' 或 'no'") - } - if packageConfirm == "no" { - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: "用户取消了包的创建操作", - }, - }, - }, nil - } - } - - if plan.NeedCreatedModules { - modulesConfirm, ok := request.GetArguments()["modulesConfirm"].(string) - if !ok || (modulesConfirm != "yes" && modulesConfirm != "no") { - return nil, errors.New("参数错误:当需要创建模块时,modulesConfirm 必须是 'yes' 或 'no'") - } - if modulesConfirm == "no" { - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: "用户取消了模块的创建操作", - }, - }, - }, nil - } - } - - // 执行创建操作 - result := t.executeCreation(ctx, &plan) - - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - return nil, fmt.Errorf("序列化结果失败: %v", err) - } - - // 添加权限分配提醒 - permissionReminder := "\n\n⚠️ 重要提醒:\n" + - "模块创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的API和菜单权限," + - "以确保用户能够正常访问新功能。\n" + - "具体步骤:\n" + - "1. 进入角色管理页面\n" + - "2. 选择需要授权的角色\n" + - "3. 在API权限中勾选新创建的API接口\n" + - "4. 在菜单权限中勾选新创建的菜单项\n" + - "5. 保存权限配置" - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("执行结果:\n\n%s%s", string(resultJSON), permissionReminder), - }, - }, - }, nil -} - -// isSystemFunction 判断是否为系统功能 -func (t *AutomationModuleAnalyzer) isSystemFunction(requirement string) bool { - systemKeywords := []string{ - "用户", "权限", "角色", "菜单", "系统", "配置", "字典", "参数", - "user", "authority", "role", "menu", "system", "config", "dictionary", - "认证", "授权", "登录", "注册", "JWT", "casbin", - } - - requirementLower := strings.ToLower(requirement) - for _, keyword := range systemKeywords { - if strings.Contains(requirementLower, keyword) { - return true - } - } - return false -} - -// buildDirectoryStructure 构建目录结构信息 -func (t *AutomationModuleAnalyzer) buildDirectoryStructure(plan *ExecutionPlan) map[string]string { - paths := make(map[string]string) - - // 获取配置信息 - autoCodeConfig := global.GVA_CONFIG.AutoCode - - // 构建基础路径 - rootPath := autoCodeConfig.Root - serverPath := autoCodeConfig.Server - webPath := autoCodeConfig.Web - moduleName := autoCodeConfig.Module - - // 如果计划中有包名,使用计划中的包名,否则使用默认 - packageName := "example" - if plan.PackageInfo != nil && plan.PackageInfo.PackageName != "" { - packageName = plan.PackageInfo.PackageName - } - - // 如果计划中有模块信息,获取第一个模块的结构名作为默认值 - structName := "ExampleStruct" - if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" { - structName = plan.ModulesInfo[0].StructName - } - - // 根据包类型构建不同的路径结构 - packageType := plan.PackageType - if packageType == "" { - packageType = "package" // 默认为package模式 - } - - // 构建服务端路径 - if serverPath != "" { - serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath) - - if packageType == "plugin" { - // Plugin 模式:所有文件都在 /plugin/packageName/ 目录下 - pluginBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName) - - // API 路径 - paths["api"] = fmt.Sprintf("%s/api", pluginBasePath) - - // Service 路径 - paths["service"] = fmt.Sprintf("%s/service", pluginBasePath) - - // Model 路径 - paths["model"] = fmt.Sprintf("%s/model", pluginBasePath) - - // Router 路径 - paths["router"] = fmt.Sprintf("%s/router", pluginBasePath) - - // Request 路径 - paths["request"] = fmt.Sprintf("%s/model/request", pluginBasePath) - - // Response 路径 - paths["response"] = fmt.Sprintf("%s/model/response", pluginBasePath) - - // Plugin 特有文件 - paths["plugin_main"] = fmt.Sprintf("%s/main.go", pluginBasePath) - paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", pluginBasePath) - paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", pluginBasePath) - } else { - // Package 模式:传统的目录结构 - // API 路径 - paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName) - - // Service 路径 - paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName) - - // Model 路径 - paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName) - - // Router 路径 - paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName) - - // Request 路径 - paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName) - - // Response 路径 - paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName) - } - } - - // 构建前端路径(两种模式相同) - if webPath != "" { - webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath) - - // Vue 页面路径 - paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName) - - // API 路径 - paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName) - } - - // 添加模块信息 - paths["module"] = moduleName - paths["package_name"] = packageName - paths["package_type"] = packageType - paths["struct_name"] = structName - paths["root_path"] = rootPath - paths["server_path"] = serverPath - paths["web_path"] = webPath - - return paths -} - -// validateExecutionPlan 验证执行计划的完整性 -func (t *AutomationModuleAnalyzer) validateExecutionPlan(plan *ExecutionPlan) error { - // 验证基本字段 - if plan.PackageName == "" { - return errors.New("packageName 不能为空") - } - if plan.PackageType != "package" && plan.PackageType != "plugin" { - return errors.New("packageType 必须是 'package' 或 'plugin'") - } - - // 验证packageType和template字段的一致性 - if plan.NeedCreatedPackage && plan.PackageInfo != nil { - if plan.PackageType != plan.PackageInfo.Template { - return errors.New("packageType 和 packageInfo.template 必须保持一致") - } - } - - // 验证包信息 - if plan.NeedCreatedPackage { - if plan.PackageInfo == nil { - return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空") - } - if plan.PackageInfo.PackageName == "" { - return errors.New("packageInfo.packageName 不能为空") - } - if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" { - return errors.New("packageInfo.template 必须是 'package' 或 'plugin'") - } - if plan.PackageInfo.Label == "" { - return errors.New("packageInfo.label 不能为空") - } - if plan.PackageInfo.Desc == "" { - return errors.New("packageInfo.desc 不能为空") - } - } - - // 验证模块信息(批量验证) - if plan.NeedCreatedModules { - if len(plan.ModulesInfo) == 0 { - return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空") - } - - // 遍历验证每个模块 - for moduleIndex, moduleInfo := range plan.ModulesInfo { - if moduleInfo.Package == "" { - return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1) - } - if moduleInfo.StructName == "" { - return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1) - } - if moduleInfo.TableName == "" { - return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1) - } - if moduleInfo.Description == "" { - return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1) - } - if moduleInfo.Abbreviation == "" { - return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1) - } - if moduleInfo.PackageName == "" { - return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1) - } - if moduleInfo.HumpPackageName == "" { - return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1) - } - - // 验证字段信息 - if len(moduleInfo.Fields) == 0 { - return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1) - } - - for i, field := range moduleInfo.Fields { - if field.FieldName == "" { - return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1) - } - - if field.FieldDesc == "" { - return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1) - } - if field.FieldType == "" { - return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1) - } - if field.FieldJson == "" { - return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1) - } - if field.ColumnName == "" { - return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1) - } - - // 确保字段名首字母大写 - if len(field.FieldName) > 0 { - firstChar := string(field.FieldName[0]) - if firstChar >= "a" && firstChar <= "z" { - moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:] - } - } - - // 确保FieldJson使用小驼峰命名 - if len(field.FieldJson) > 0 { - // 处理下划线命名转小驼峰 - if strings.Contains(field.FieldJson, "_") { - parts := strings.Split(field.FieldJson, "_") - camelCase := strings.ToLower(parts[0]) - for j := 1; j < len(parts); j++ { - if len(parts[j]) > 0 { - camelCase += strings.ToUpper(string(parts[j][0])) + strings.ToLower(parts[j][1:]) - } - } - moduleInfo.Fields[i].FieldJson = camelCase - } else { - // 处理首字母大写转小写 - firstChar := string(field.FieldJson[0]) - if firstChar >= "A" && firstChar <= "Z" { - moduleInfo.Fields[i].FieldJson = strings.ToLower(firstChar) + field.FieldJson[1:] - } - } - } - - // 确保ColumnName使用下划线命名 - if len(field.ColumnName) > 0 { - // 将驼峰命名转换为下划线命名 - var result strings.Builder - for i, r := range field.ColumnName { - if i > 0 && r >= 'A' && r <= 'Z' { - result.WriteRune('_') - } - result.WriteRune(unicode.ToLower(r)) - } - moduleInfo.Fields[i].ColumnName = result.String() - } - - // 验证字段类型 - validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"} - validType := false - for _, validFieldType := range validFieldTypes { - if field.FieldType == validFieldType { - validType = true - break - } - } - if !validType { - return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes) - } - - // 验证搜索类型(如果设置了) - if field.FieldSearchType != "" { - validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"} - validSearchType := false - for _, validType := range validSearchTypes { - if field.FieldSearchType == validType { - validSearchType = true - break - } - } - if !validSearchType { - return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes) - } - } - } - - // 验证主键设置 - if !moduleInfo.GvaModel { - // 当不使用GVA模型时,必须有且仅有一个字段设置为主键 - primaryKeyCount := 0 - for _, field := range moduleInfo.Fields { - if field.PrimaryKey { - primaryKeyCount++ - } - } - if primaryKeyCount == 0 { - return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1) - } - if primaryKeyCount > 1 { - return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1) - } - } else { - // 当使用GVA模型时,所有字段的primaryKey都应该为false - for i, field := range moduleInfo.Fields { - if field.PrimaryKey { - return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", moduleIndex+1, i+1) - } - } - } - } - } - - return nil -} - -// executeCreation 执行创建操作 -func (t *AutomationModuleAnalyzer) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecutionResult { - result := &ExecutionResult{ - Success: false, - Paths: make(map[string]string), - } - - // 无论如何都先构建目录结构信息,确保paths始终返回 - result.Paths = t.buildDirectoryStructure(plan) - - if !plan.NeedCreatedModules { - result.Success = true - result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; " - return result - } - - // 创建包(如果需要) - if plan.NeedCreatedPackage && plan.PackageInfo != nil { - packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage - err := packageService.Create(ctx, plan.PackageInfo) - if err != nil { - result.Message = fmt.Sprintf("创建包失败: %v", err) - // 即使创建包失败,也要返回paths信息 - return result - } - result.Message += "包创建成功; " - } - - // 批量创建字典和模块(如果需要) - if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { - templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate - - // 先批量创建所有模块需要的字典 - dictResult := t.createRequiredDictionaries(ctx, plan.ModulesInfo) - result.Message += dictResult - - // 遍历所有模块进行创建 - for _, moduleInfo := range plan.ModulesInfo { - - // 创建模块 - err := moduleInfo.Pretreatment() - if err != nil { - result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err) - continue // 继续处理下一个模块 - } - - err = templateService.Create(ctx, *moduleInfo) - if err != nil { - result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err) - continue // 继续处理下一个模块 - } - result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName) - } - - result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo)) - - // 添加重要提醒:不要使用其他MCP工具 - result.Message += "\n\n⚠️ 重要提醒:\n" - result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n" - result.Message += "- api_creator:API权限已在模块创建时自动生成\n" - result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n" - result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n" - } - - result.Message += "已构建目录结构信息; " - result.Success = true - - if result.Message == "" { - result.Message = "执行计划完成" - } - - return result -} - -// createRequiredDictionaries 创建所需的字典(批量处理) -func (t *AutomationModuleAnalyzer) createRequiredDictionaries(ctx context.Context, modulesInfoList []*request.AutoCode) string { - var messages []string - dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService - createdDictTypes := make(map[string]bool) // 用于避免重复创建相同的字典 - - // 遍历所有模块 - for moduleIndex, modulesInfo := range modulesInfoList { - messages = append(messages, fmt.Sprintf("处理模块 %d (%s) 的字典: ", moduleIndex+1, modulesInfo.StructName)) - - // 遍历当前模块的所有字段,查找使用字典的字段 - moduleHasDictFields := false - for _, field := range modulesInfo.Fields { - if field.DictType != "" { - moduleHasDictFields = true - - // 如果这个字典类型已经在之前的模块中创建过,跳过 - if createdDictTypes[field.DictType] { - messages = append(messages, fmt.Sprintf("字典 %s 已在前面的模块中创建,跳过; ", field.DictType)) - continue - } - - // 检查字典是否存在 - exists, err := t.checkDictionaryExists(field.DictType) - if err != nil { - messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", field.DictType, err)) - continue - } - - if !exists { - // 字典不存在,创建字典 - dictionary := model.SysDictionary{ - Name: t.generateDictionaryName(field.DictType, field.FieldDesc), - Type: field.DictType, - Status: &[]bool{true}[0], // 默认启用 - Desc: fmt.Sprintf("自动生成的字典,用于模块 %s 字段: %s (%s)", modulesInfo.StructName, field.FieldName, field.FieldDesc), - } - - err = dictionaryService.CreateSysDictionary(dictionary) - if err != nil { - messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", field.DictType, err)) - } else { - messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", field.DictType, dictionary.Name)) - createdDictTypes[field.DictType] = true // 标记为已创建 - - // 创建默认的字典详情项 - t.createDefaultDictionaryDetails(ctx, field.DictType, field.FieldDesc) - } - } else { - messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", field.DictType)) - createdDictTypes[field.DictType] = true // 标记为已存在 - } - } - } - - if !moduleHasDictFields { - messages = append(messages, "无需创建字典; ") - } - } - - if len(messages) == 0 { - return "未发现需要创建的字典; " - } - - return strings.Join(messages, "") -} - -// checkDictionaryExists 检查字典是否存在 -func (t *AutomationModuleAnalyzer) checkDictionaryExists(dictType string) (bool, error) { - var dictionary model.SysDictionary - err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil // 字典不存在 - } - return false, err // 其他错误 - } - return true, nil // 字典存在 -} - -// generateDictionaryName 生成字典名称 -func (t *AutomationModuleAnalyzer) generateDictionaryName(dictType, fieldDesc string) string { - if fieldDesc != "" { - return fmt.Sprintf("%s字典", fieldDesc) - } - return fmt.Sprintf("%s字典", dictType) -} - -// createDefaultDictionaryDetails 创建默认的字典详情项 -func (t *AutomationModuleAnalyzer) createDefaultDictionaryDetails(ctx context.Context, dictType, fieldDesc string) { - // 字典选项现在通过 generate_dictionary_options MCP工具由AI client传入 - // 这里不再创建默认选项,只是保留方法以保持兼容性 - global.GVA_LOG.Info(fmt.Sprintf("字典 %s 已创建,请使用 generate_dictionary_options 工具添加字典选项", dictType)) -} - -// DictionaryOption 字典选项结构 -type DictionaryOption struct { - Label string `json:"label"` - Value string `json:"value"` - Sort int `json:"sort"` -} - -// generateSmartDictionaryOptions 通过MCP调用让AI生成字典选项 -func (t *AutomationModuleAnalyzer) generateSmartDictionaryOptions(dictType, fieldDesc string) []struct { - label string - value string - sort int -} { - // 返回空切片,不再使用预制选项 - // 字典选项将通过新的MCP工具由AI client传入 - return []struct { - label string - value string - sort int - }{} -} - -// detectPluginIntent 检测用户需求中是否包含插件相关的关键词 -func (t *AutomationModuleAnalyzer) detectPluginIntent(requirement string) (suggestedType string, isPlugin bool, confidence string) { - // 转换为小写进行匹配 - requirementLower := strings.ToLower(requirement) - - // 插件相关关键词 - pluginKeywords := []string{ - "插件", "plugin", "扩展", "extension", "addon", "模块插件", - "功能插件", "业务插件", "第三方插件", "自定义插件", - } - - // 包相关关键词(用于排除误判) - packageKeywords := []string{ - "包", "package", "模块包", "业务包", "功能包", - } - - // 检测插件关键词 - pluginMatches := 0 - for _, keyword := range pluginKeywords { - if strings.Contains(requirementLower, keyword) { - pluginMatches++ - } - } - - // 检测包关键词 - packageMatches := 0 - for _, keyword := range packageKeywords { - if strings.Contains(requirementLower, keyword) { - packageMatches++ - } - } - - // 决策逻辑 - if pluginMatches > 0 { - if packageMatches == 0 || pluginMatches > packageMatches { - return "plugin", true, "高" - } else { - return "plugin", true, "中" - } - } - - // 默认返回package - return "package", false, "低" -} - -// isPackageFolderEmpty 检查包对应的文件夹是否为空 -func (t *AutomationModuleAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) { - // 根据模板类型确定基础路径 - var basePath string - if template == "plugin" { - basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) - } else { - // package 类型 - basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName) - } - - // 检查文件夹是否存在 - if _, err := os.Stat(basePath); os.IsNotExist(err) { - // 文件夹不存在,认为是空的 - return true, nil - } else if err != nil { - return false, fmt.Errorf("检查文件夹状态失败: %v", err) - } - - // 读取文件夹内容 - entries, err := os.ReadDir(basePath) - if err != nil { - return false, fmt.Errorf("读取文件夹内容失败: %v", err) - } - - // 检查目录下是否有 .go 文件 - hasGoFiles := false - for _, entry := range entries { - name := entry.Name() - // 跳过隐藏文件、.DS_Store 等系统文件 - if strings.HasPrefix(name, ".") { - continue - } - // 如果是目录,递归检查子目录中的 .go 文件 - if entry.IsDir() { - subPath := filepath.Join(basePath, name) - subEntries, err := os.ReadDir(subPath) - if err != nil { - continue - } - for _, subEntry := range subEntries { - if !subEntry.IsDir() && strings.HasSuffix(subEntry.Name(), ".go") { - hasGoFiles = true - break - } - } - if hasGoFiles { - break - } - } else if strings.HasSuffix(name, ".go") { - // 如果是 .go 文件 - hasGoFiles = true - break - } - } - - // 如果没有 .go 文件,认为是空包 - return !hasGoFiles, nil -} - -// removeEmptyPackageFolder 删除空的包文件夹 -func (t *AutomationModuleAnalyzer) removeEmptyPackageFolder(packageName, template string) error { - var errors []string - - if template == "plugin" { - // plugin 类型只删除 plugin 目录下的文件夹 - basePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) - if err := t.removeDirectoryIfExists(basePath); err != nil { - errors = append(errors, fmt.Sprintf("删除plugin文件夹失败: %v", err)) - } - } else { - // package 类型需要删除多个目录下的相关文件 - paths := []string{ - filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName), - filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName), - filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName), - filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName), - } - - for _, path := range paths { - if err := t.removeDirectoryIfExists(path); err != nil { - errors = append(errors, fmt.Sprintf("删除%s失败: %v", path, err)) - } - } - } - - if len(errors) > 0 { - return fmt.Errorf("删除过程中出现错误: %s", strings.Join(errors, "; ")) - } - - return nil -} - -// removeDirectoryIfExists 删除目录(如果存在) -func (t *AutomationModuleAnalyzer) removeDirectoryIfExists(dirPath string) error { - // 检查文件夹是否存在 - if _, err := os.Stat(dirPath); os.IsNotExist(err) { - // 文件夹不存在,无需删除 - return nil - } else if err != nil { - return fmt.Errorf("检查文件夹状态失败: %v", err) - } - - // 删除文件夹及其所有内容 - if err := os.RemoveAll(dirPath); err != nil { - return fmt.Errorf("删除文件夹失败: %v", err) - } - - global.GVA_LOG.Info(fmt.Sprintf("成功删除目录: %s", dirPath)) - return nil -} - -// cleanupRelatedApiAndMenus 清理与删除的模块相关的API和菜单记录 -func (t *AutomationModuleAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error { - if len(historyIDs) == 0 { - return nil - } - - // 获取要删除的历史记录信息 - var histories []model.SysAutoCodeHistory - if err := global.GVA_DB.Where("id IN ?", historyIDs).Find(&histories).Error; err != nil { - return fmt.Errorf("获取历史记录失败: %v", err) - } - - var deletedApiCount, deletedMenuCount int - - for _, history := range histories { - // 删除相关的API记录(使用存储的API IDs) - if len(history.ApiIDs) > 0 { - ids := make([]int, 0, len(history.ApiIDs)) - for _, id := range history.ApiIDs { - ids = append(ids, int(id)) - } - idsReq := common.IdsReq{Ids: ids} - if err := systemService.ApiServiceApp.DeleteApisByIds(idsReq); err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("删除API记录失败 (模块: %s): %v", history.StructName, err)) - } else { - deletedApiCount += len(ids) - global.GVA_LOG.Info(fmt.Sprintf("成功删除API记录 (模块: %s, 数量: %d)", history.StructName, len(ids))) - } - } - - // 删除相关的菜单记录(使用存储的菜单ID) - if history.MenuID != 0 { - if err := systemService.BaseMenuServiceApp.DeleteBaseMenu(int(history.MenuID)); err != nil { - global.GVA_LOG.Warn(fmt.Sprintf("删除菜单记录失败 (模块: %s, 菜单ID: %d): %v", history.StructName, history.MenuID, err)) - } else { - deletedMenuCount++ - global.GVA_LOG.Info(fmt.Sprintf("成功删除菜单记录 (模块: %s, 菜单ID: %d)", history.StructName, history.MenuID)) - } - } - } - - if deletedApiCount > 0 || deletedMenuCount > 0 { - global.GVA_LOG.Info(fmt.Sprintf("清理完成:删除了 %d 个API记录和 %d 个菜单记录", deletedApiCount, deletedMenuCount)) - } - - return nil -} diff --git a/server/mcp/gva_execute.go b/server/mcp/gva_execute.go new file mode 100644 index 0000000000..62377b8502 --- /dev/null +++ b/server/mcp/gva_execute.go @@ -0,0 +1,778 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "strings" + + "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/service" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + RegisterTool(&GVAExecutor{}) +} + +// GVAExecutor GVA代码生成器 +type GVAExecutor struct{} + +// ExecuteRequest 执行请求结构 +type ExecuteRequest struct { + ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划 + Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录) +} + +// ExecuteResponse 执行响应结构 +type ExecuteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + PackageID uint `json:"packageId,omitempty"` + HistoryID uint `json:"historyId,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + GeneratedPaths []string `json:"generatedPaths,omitempty"` + NextActions []string `json:"nextActions,omitempty"` +} + +// ExecutionPlan 执行计划结构 +type ExecutionPlan struct { + PackageName string `json:"packageName"` + PackageType string `json:"packageType"` // "plugin" 或 "package" + NeedCreatedPackage bool `json:"needCreatedPackage"` + NeedCreatedModules bool `json:"needCreatedModules"` + NeedCreatedDictionaries bool `json:"needCreatedDictionaries"` + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` + ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + DictionariesInfo []*DictionaryGenerateRequest `json:"dictionariesInfo,omitempty"` +} + +// New 创建GVA代码生成执行器工具 +func (g *GVAExecutor) New() mcp.Tool { + return mcp.NewTool("gva_execute", + mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤** + +**核心功能:** +- 根据需求分析和当前的包信息判断是否调用,如果需要调用,则根据入参描述生成json,用于直接生成代码 +- 支持批量创建多个模块 +- 自动创建包、模块、字典等 +- 移除了确认步骤,提高执行效率 + +**使用场景:** +- 在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp +- 根据分析结果直接生成代码 +- 适用于自动化代码生成流程 + +**批量创建功能:** +- 支持在单个ExecutionPlan中创建多个模块 +- modulesInfo字段为数组,可包含多个模块配置 +- 一次性处理多个模块的创建和字典生成 + +**新功能:自动字典创建** +- 当结构体字段使用了字典类型(dictType不为空)时,系统会自动检查字典是否存在 +- 如果字典不存在,会自动创建对应的字典及默认的字典详情项 +- 字典创建包括:字典主表记录和默认的选项值(选项1、选项2等) + +**重要限制:** +- 当needCreatedModules=true时,模块创建会自动生成API和菜单,因此不应再调用api_creator和menu_creator工具 +- 只有在单独创建API或菜单(不涉及模块创建)时才使用api_creator和menu_creator工具 + +重要:ExecutionPlan结构体格式要求(支持批量创建): +{ + "packageName": "包名(string)", + "packageType": "package或plugin(string),如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。", + "needCreatedPackage": "是否需要创建包(bool)", + "needCreatedModules": "是否需要创建模块(bool)", + "needCreatedDictionaries": "是否需要创建字典(bool)", + "packageInfo": { + "desc": "描述(string)", + "label": "展示名(string)", + "template": "package或plugin(string),如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。", + "packageName": "包名(string)" + }, + "modulesInfo": [{ + "package": "包名(string,必然是小写开头)", + "tableName": "数据库表名(string,使用蛇形命名法)", + "businessDB": "业务数据库(string)", + "structName": "结构体名(string)", + "packageName": "文件名称(string)", + "description": "中文描述(string)", + "abbreviation": "简称(string)", + "humpPackageName": "文件名称 一般是结构体名的小驼峰(string)", + "gvaModel": "是否使用GVA模型(bool) 固定为true 后续不需要创建ID created_at deleted_at updated_at", + "autoMigrate": "是否自动迁移(bool)", + "autoCreateResource": "是否创建资源(bool,默认为false)", + "autoCreateApiToSql": "是否创建API(bool,默认为true)", + "autoCreateMenuToSql": "是否创建菜单(bool,默认为true)", + "autoCreateBtnAuth": "是否创建按钮权限(bool,默认为false)", + "onlyTemplate": "是否仅模板(bool,默认为false)", + "isTree": "是否树形结构(bool,默认为false)", + "treeJson": "树形JSON字段(string)", + "isAdd": "是否新增(bool) 固定为false", + "generateWeb": "是否生成前端(bool)", + "generateServer": "是否生成后端(bool)", + "fields": [{ + "fieldName": "字段名(string)必须大写开头", + "fieldDesc": "字段描述(string)", + "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)", + "fieldJson": "JSON标签(string)", + "dataTypeLong": "数据长度(string)", + "comment": "注释(string)", + "columnName": "数据库列名(string)", + "fieldSearchType": "搜索类型:=/>/=/<=/NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN等(string)", + "fieldSearchHide": "是否隐藏搜索(bool)", + "dictType": "字典类型(string)", + "form": "表单显示(bool)", + "table": "表格显示(bool)", + "desc": "详情显示(bool)", + "excel": "导入导出(bool)", + "require": "是否必填(bool)", + "defaultValue": "默认值(string)", + "errorText": "错误提示(string)", + "clearable": "是否可清空(bool)", + "sort": "是否排序(bool)", + "primaryKey": "是否主键(bool)", + "dataSource": "数据源配置(object) - 用于配置字段的关联表信息,结构:{\"dbName\":\"数据库名\",\"table\":\"关联表名\",\"label\":\"显示字段\",\"value\":\"值字段\",\"association\":1或2(1=一对一,2=一对多),\"hasDeletedAt\":true/false}。\n\n**获取表名提示:**\n- 可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名\n- 例如:SysUser 的表名为 \"sys_users\",ExaFileUploadAndDownload 的表名为 \"exa_file_upload_and_downloads\"\n- 插件模块示例:Info 的表名为 \"gva_announcements_info\"\n\n**获取数据库名提示:**\n- 主数据库:通常使用 \"gva\"(默认数据库标识)\n- 多数据库:可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段\n- 如果用户未提及关联多数据库信息 则使用默认数据库 默认数据库的情况下 dbName此处填写为空", + "checkDataSource": "是否检查数据源(bool) - 启用后会验证关联表的存在性", + "fieldIndexType": "索引类型(string)" + }] + }, { + "package": "包名(string)", + "tableName": "第二个模块的表名(string)", + "structName": "第二个模块的结构体名(string)", + "description": "第二个模块的描述(string)", + "...": "更多模块配置..." + }], + "dictionariesInfo":[{ + "dictType": "字典类型(string) - 用于标识字典的唯一性", + "dictName": "字典名称(string) - 必须生成,字典的中文名称", + "description": "字典描述(string) - 字典的用途说明", + "status": "字典状态(bool) - true启用,false禁用", + "fieldDesc": "字段描述(string) - 用于AI理解字段含义并生成合适的选项", + "options": [{ + "label": "显示名称(string) - 用户看到的选项名", + "value": "选项值(string) - 实际存储的值", + "sort": "排序号(int) - 数字越小越靠前" + }] + }] +} + +注意: +1. needCreatedPackage=true时packageInfo必需 +2. needCreatedModules=true时modulesInfo必需 +3. needCreatedDictionaries=true时dictionariesInfo必需 +4. dictionariesInfo中的options字段可选,如果不提供将根据fieldDesc自动生成默认选项 +5. 字典创建会在模块创建之前执行,确保模块字段可以正确引用字典类型 +6. packageType只能是"package"或"plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。" +7. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组) +8. 搜索类型支持:=,!=,>,>=,<,<=,NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN +9. gvaModel=true时自动包含ID,CreatedAt,UpdatedAt,DeletedAt字段 +10. **重要**:当gvaModel=false时,必须有一个字段的primaryKey=true,否则会导致PrimaryField为nil错误 +11. **重要**:当gvaModel=true时,系统会自动设置ID字段为主键,无需手动设置primaryKey=true +12. 智能字典创建功能:当字段使用字典类型(DictType)时,系统会: + - 自动检查字典是否存在,如果不存在则创建字典 + - 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景 + - 为无法识别的字典类型提供通用默认选项 +13. **模块关联配置**:当需要配置模块间的关联关系时,使用dataSource字段: + - **dbName**: 关联的数据库名称 + - **table**: 关联的表名 + - **label**: 用于显示的字段名(如name、title等) + - **value**: 用于存储的值字段名(通常是id) + - **association**: 关联关系类型(1=一对一关联,2=一对多关联)一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个,则选用一对一,如果他需要关联多个他的关联实体,则选用一对多。 + - **hasDeletedAt**: 关联表是否有软删除字段 + - **checkDataSource**: 设为true时会验证关联表的存在性 + - 示例:{"dbName":"","table":"sys_users","label":"username","value":"id","association":1,"hasDeletedAt":true} +14. **自动字段类型修正**:系统会自动检查和修正字段类型: + - 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array' + - 这确保了一对多关联数据的正确存储和处理 + - 修正操作会记录在日志中,便于开发者了解变更情况`), + mcp.WithObject("executionPlan", + mcp.Description("执行计划,包含包信息和模块信息"), + mcp.Required(), + ), + mcp.WithString("requirement", + mcp.Description("原始需求描述(可选,用于日志记录)"), + ), + ) +} + +// Handle 处理执行请求(移除确认步骤) +func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + executionPlanData, ok := request.GetArguments()["executionPlan"] + if !ok { + return nil, errors.New("参数错误:executionPlan 必须提供") + } + + // 解析执行计划 + planJSON, err := json.Marshal(executionPlanData) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v", err) + } + + var plan ExecutionPlan + err = json.Unmarshal(planJSON, &plan) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) + } + + // 验证执行计划的完整性 + if err := g.validateExecutionPlan(&plan); err != nil { + return nil, fmt.Errorf("执行计划验证失败: %v", err) + } + + // 获取原始需求(可选) + var originalRequirement string + if reqData, ok := request.GetArguments()["requirement"]; ok { + if reqStr, ok := reqData.(string); ok { + originalRequirement = reqStr + } + } + + // 直接执行创建操作(无确认步骤) + result := g.executeCreation(ctx, &plan) + + // 如果执行成功且有原始需求,提供代码复检建议 + var reviewMessage string + if result.Success && originalRequirement != "" { + global.GVA_LOG.Info("执行完成,返回生成的文件路径供AI进行代码复检...") + + // 构建文件路径信息供AI使用 + var pathsInfo []string + for _, path := range result.GeneratedPaths { + pathsInfo = append(pathsInfo, fmt.Sprintf("- %s", path)) + } + + reviewMessage = fmt.Sprintf("\n\n📁 已生成以下文件:\n%s\n\n💡 提示:可以检查生成的代码是否满足原始需求。", strings.Join(pathsInfo, "\n")) + } else if originalRequirement == "" { + reviewMessage = "\n\n💡 提示:如需代码复检,请提供原始需求描述。" + } + + // 序列化响应 + response := ExecuteResponse{ + Success: result.Success, + Message: result.Message, + PackageID: result.PackageID, + HistoryID: result.HistoryID, + Paths: result.Paths, + GeneratedPaths: result.GeneratedPaths, + NextActions: result.NextActions, + } + + responseJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("执行结果:\n\n%s%s", string(responseJSON), reviewMessage)), + }, + }, nil +} + +// validateExecutionPlan 验证执行计划的完整性 +func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error { + // 验证基本字段 + if plan.PackageName == "" { + return errors.New("packageName 不能为空") + } + if plan.PackageType != "package" && plan.PackageType != "plugin" { + return errors.New("packageType 必须是 'package' 或 'plugin'") + } + + // 验证packageType和template字段的一致性 + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + if plan.PackageType != plan.PackageInfo.Template { + return errors.New("packageType 和 packageInfo.template 必须保持一致") + } + } + + // 验证包信息 + if plan.NeedCreatedPackage { + if plan.PackageInfo == nil { + return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空") + } + if plan.PackageInfo.PackageName == "" { + return errors.New("packageInfo.packageName 不能为空") + } + if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" { + return errors.New("packageInfo.template 必须是 'package' 或 'plugin'") + } + if plan.PackageInfo.Label == "" { + return errors.New("packageInfo.label 不能为空") + } + if plan.PackageInfo.Desc == "" { + return errors.New("packageInfo.desc 不能为空") + } + } + + // 验证模块信息(批量验证) + if plan.NeedCreatedModules { + if len(plan.ModulesInfo) == 0 { + return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空") + } + + // 遍历验证每个模块 + for moduleIndex, moduleInfo := range plan.ModulesInfo { + if moduleInfo.Package == "" { + return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1) + } + if moduleInfo.StructName == "" { + return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1) + } + if moduleInfo.TableName == "" { + return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1) + } + if moduleInfo.Description == "" { + return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1) + } + if moduleInfo.Abbreviation == "" { + return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1) + } + if moduleInfo.PackageName == "" { + return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1) + } + if moduleInfo.HumpPackageName == "" { + return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1) + } + + // 验证字段信息 + if len(moduleInfo.Fields) == 0 { + return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1) + } + + for i, field := range moduleInfo.Fields { + if field.FieldName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1) + } + + // 确保字段名首字母大写 + if len(field.FieldName) > 0 { + firstChar := string(field.FieldName[0]) + if firstChar >= "a" && firstChar <= "z" { + moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:] + } + } + if field.FieldDesc == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1) + } + if field.FieldType == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1) + } + if field.FieldJson == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1) + } + if field.ColumnName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1) + } + + // 验证字段类型 + validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"} + validType := false + for _, validFieldType := range validFieldTypes { + if field.FieldType == validFieldType { + validType = true + break + } + } + if !validType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes) + } + + // 验证搜索类型(如果设置了) + if field.FieldSearchType != "" { + validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"} + validSearchType := false + for _, validType := range validSearchTypes { + if field.FieldSearchType == validType { + validSearchType = true + break + } + } + if !validSearchType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes) + } + } + + // 验证 dataSource 字段配置 + if field.DataSource != nil { + associationValue := field.DataSource.Association + // 当 association 为 2(一对多关联)时,强制修改 fieldType 为 array + if associationValue == 2 { + if field.FieldType != "array" { + global.GVA_LOG.Info(fmt.Sprintf("模块 %d 字段 %d:检测到一对多关联(association=2),自动将 fieldType 从 '%s' 修改为 'array'", moduleIndex+1, i+1, field.FieldType)) + moduleInfo.Fields[i].FieldType = "array" + } + } + + // 验证 association 值的有效性 + if associationValue != 1 && associationValue != 2 { + return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1(一对一)或 2(一对多)", moduleIndex+1, i+1) + } + } + } + + // 验证主键设置 + if !moduleInfo.GvaModel { + // 当不使用GVA模型时,必须有且仅有一个字段设置为主键 + primaryKeyCount := 0 + for _, field := range moduleInfo.Fields { + if field.PrimaryKey { + primaryKeyCount++ + } + } + if primaryKeyCount == 0 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1) + } + if primaryKeyCount > 1 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1) + } + } else { + // 当使用GVA模型时,所有字段的primaryKey都应该为false + for i, field := range moduleInfo.Fields { + if field.PrimaryKey { + return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", moduleIndex+1, i+1) + } + } + } + } + } + + return nil +} + +// executeCreation 执行创建操作 +func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecuteResponse { + result := &ExecuteResponse{ + Success: false, + Paths: make(map[string]string), + GeneratedPaths: []string{}, // 初始化生成文件路径列表 + } + + // 无论如何都先构建目录结构信息,确保paths始终返回 + result.Paths = g.buildDirectoryStructure(plan) + + // 记录预期生成的文件路径 + result.GeneratedPaths = g.collectExpectedFilePaths(plan) + + if !plan.NeedCreatedModules { + result.Success = true + result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; " + return result + } + + // 创建包(如果需要) + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage + err := packageService.Create(ctx, plan.PackageInfo) + if err != nil { + result.Message = fmt.Sprintf("创建包失败: %v", err) + // 即使创建包失败,也要返回paths信息 + return result + } + result.Message += "包创建成功; " + } + + // 创建指定字典(如果需要) + if plan.NeedCreatedDictionaries && len(plan.DictionariesInfo) > 0 { + dictResult := g.createDictionariesFromInfo(ctx, plan.DictionariesInfo) + result.Message += dictResult + } + + // 批量创建字典和模块(如果需要) + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate + + // 遍历所有模块进行创建 + for _, moduleInfo := range plan.ModulesInfo { + + // 创建模块 + err := moduleInfo.Pretreatment() + if err != nil { + result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + + err = templateService.Create(ctx, *moduleInfo) + if err != nil { + result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName) + } + + result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo)) + + // 添加重要提醒:不要使用其他MCP工具 + result.Message += "\n\n⚠️ 重要提醒:\n" + result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n" + result.Message += "- api_creator:API权限已在模块创建时自动生成\n" + result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n" + result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n" + } + + result.Message += "已构建目录结构信息; " + result.Success = true + + if result.Message == "" { + result.Message = "执行计划完成" + } + + return result +} + +// buildDirectoryStructure 构建目录结构信息 +func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]string { + paths := make(map[string]string) + + // 获取配置信息 + autoCodeConfig := global.GVA_CONFIG.AutoCode + + // 构建基础路径 + rootPath := autoCodeConfig.Root + serverPath := autoCodeConfig.Server + webPath := autoCodeConfig.Web + moduleName := autoCodeConfig.Module + + // 如果计划中有包名,使用计划中的包名,否则使用默认 + packageName := "example" + if plan.PackageName != "" { + packageName = plan.PackageName + } + + // 如果计划中有模块信息,获取第一个模块的结构名作为默认值 + structName := "ExampleStruct" + if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" { + structName = plan.ModulesInfo[0].StructName + } + + // 根据包类型构建不同的路径结构 + packageType := plan.PackageType + if packageType == "" { + packageType = "package" // 默认为package模式 + } + + // 构建服务端路径 + if serverPath != "" { + serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath) + + if packageType == "plugin" { + // Plugin 模式:所有文件都在 /plugin/packageName/ 目录下 + plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName) + + // API 路径 + paths["api"] = fmt.Sprintf("%s/api", plugingBasePath) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service", plugingBasePath) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model", plugingBasePath) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router", plugingBasePath) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath) + + // Plugin 特有文件 + paths["plugin_main"] = fmt.Sprintf("%s/main.go", plugingBasePath) + paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", plugingBasePath) + paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", plugingBasePath) + } else { + // Package 模式:传统的目录结构 + // API 路径 + paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName) + } + } + + // 构建前端路径(两种模式相同) + if webPath != "" { + webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath) + + if packageType == "plugin" { + // Plugin 模式:前端文件也在 /plugin/packageName/ 目录下 + pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName) + + // Vue 页面路径 + paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath) + + // API 路径 + paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath) + } else { + // Package 模式:传统的目录结构 + // Vue 页面路径 + paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName) + + // API 路径 + paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName) + } + } + + // 添加模块信息 + paths["module"] = moduleName + paths["package_name"] = packageName + paths["package_type"] = packageType + paths["struct_name"] = structName + paths["root_path"] = rootPath + paths["server_path"] = serverPath + paths["web_path"] = webPath + + return paths +} + +// collectExpectedFilePaths 收集预期生成的文件路径 +func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string { + var paths []string + + // 获取目录结构 + dirPaths := g.buildDirectoryStructure(plan) + + // 如果需要创建模块,添加预期的文件路径 + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + for _, moduleInfo := range plan.ModulesInfo { + structName := moduleInfo.StructName + + // 后端文件 + if apiPath, ok := dirPaths["api"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", apiPath, strings.ToLower(structName))) + } + if servicePath, ok := dirPaths["service"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", servicePath, strings.ToLower(structName))) + } + if modelPath, ok := dirPaths["model"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", modelPath, strings.ToLower(structName))) + } + if routerPath, ok := dirPaths["router"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", routerPath, strings.ToLower(structName))) + } + if requestPath, ok := dirPaths["request"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", requestPath, strings.ToLower(structName))) + } + if responsePath, ok := dirPaths["response"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", responsePath, strings.ToLower(structName))) + } + + // 前端文件 + if vuePage, ok := dirPaths["vue_page"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.vue", vuePage, strings.ToLower(structName))) + } + if vueApi, ok := dirPaths["vue_api"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.js", vueApi, strings.ToLower(structName))) + } + } + } + + return paths +} + +// checkDictionaryExists 检查字典是否存在 +func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) { + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + _, err := dictionaryService.GetSysDictionary(dictType, 0, nil) + if err != nil { + // 如果是记录不存在的错误,返回false + if strings.Contains(err.Error(), "record not found") { + return false, nil + } + // 其他错误返回错误信息 + return false, err + } + return true, nil +} + +// createDictionariesFromInfo 根据 DictionariesInfo 创建字典 +func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string { + var messages []string + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService + + messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo))) + + for _, dictInfo := range dictionariesInfo { + // 检查字典是否存在 + exists, err := g.checkDictionaryExists(dictInfo.DictType) + if err != nil { + messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err)) + continue + } + + if !exists { + // 字典不存在,创建字典 + dictionary := model.SysDictionary{ + Name: dictInfo.DictName, + Type: dictInfo.DictType, + Status: utils.Pointer(true), + Desc: dictInfo.Description, + } + + err = dictionaryService.CreateSysDictionary(dictionary) + if err != nil { + messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err)) + continue + } + + messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName)) + + // 获取刚创建的字典ID + var createdDict model.SysDictionary + err = global.GVA_DB.Where("type = ?", dictInfo.DictType).First(&createdDict).Error + if err != nil { + messages = append(messages, fmt.Sprintf("获取创建的字典失败: %v; ", err)) + continue + } + + // 创建字典选项 + if len(dictInfo.Options) > 0 { + successCount := 0 + for _, option := range dictInfo.Options { + dictionaryDetail := model.SysDictionaryDetail{ + Label: option.Label, + Value: option.Value, + Status: &[]bool{true}[0], // 默认启用 + Sort: option.Sort, + SysDictionaryID: int(createdDict.ID), + } + + err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail) + if err != nil { + global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err)) + } else { + successCount++ + } + } + messages = append(messages, fmt.Sprintf("创建了 %d 个字典选项; ", successCount)) + } + } else { + messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", dictInfo.DictType)) + } + } + + return strings.Join(messages, "") +} diff --git a/server/mcp/gva_review.go b/server/mcp/gva_review.go new file mode 100644 index 0000000000..a32a54478c --- /dev/null +++ b/server/mcp/gva_review.go @@ -0,0 +1,170 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/mark3labs/mcp-go/mcp" +) + +// GVAReviewer GVA代码审查工具 +type GVAReviewer struct{} + +// init 注册工具 +func init() { + RegisterTool(&GVAReviewer{}) +} + +// ReviewRequest 审查请求结构 +type ReviewRequest struct { + UserRequirement string `json:"userRequirement"` // 经过requirement_analyze后的用户需求 + GeneratedFiles []string `json:"generatedFiles"` // gva_execute创建的文件列表 +} + +// ReviewResponse 审查响应结构 +type ReviewResponse struct { + Success bool `json:"success"` // 是否审查成功 + Message string `json:"message"` // 审查结果消息 + AdjustmentPrompt string `json:"adjustmentPrompt"` // 调整代码的提示 + ReviewDetails string `json:"reviewDetails"` // 详细的审查结果 +} + +// New 创建GVA代码审查工具 +func (g *GVAReviewer) New() mcp.Tool { + return mcp.NewTool("gva_review", + mcp.WithDescription(`**GVA代码审查工具 - 在gva_execute调用后使用** + +**核心功能:** +- 接收经过requirement_analyze处理的用户需求和gva_execute生成的文件列表 +- 分析生成的代码是否满足用户的原始需求 +- 检查是否涉及到关联、交互等复杂功能 +- 如果代码不满足需求,提供调整建议和新的prompt + +**使用场景:** +- 在gva_execute成功执行后调用 +- 用于验证生成的代码是否完整满足用户需求 +- 检查模块间的关联关系是否正确实现 +- 发现缺失的交互功能或业务逻辑 + +**工作流程:** +1. 接收用户原始需求和生成的文件列表 +2. 分析需求中的关键功能点 +3. 检查生成的文件是否覆盖所有功能 +4. 识别缺失的关联关系、交互功能等 +5. 生成调整建议和新的开发prompt + +**输出内容:** +- 审查结果和是否需要调整 +- 详细的缺失功能分析 +- 针对性的代码调整建议 +- 可直接使用的开发prompt + +**重要提示:** +- 本工具专门用于代码质量审查,不执行实际的代码修改 +- 重点关注模块间关联、用户交互、业务流程完整性 +- 提供的调整建议应该具体可执行`), + mcp.WithString("userRequirement", + mcp.Description("经过requirement_analyze处理后的用户需求描述,包含详细的功能要求和字段信息"), + mcp.Required(), + ), + mcp.WithString("generatedFiles", + mcp.Description("gva_execute创建的文件列表,JSON字符串格式,包含所有生成的后端和前端文件路径"), + mcp.Required(), + ), + ) +} + +// Handle 处理审查请求 +func (g *GVAReviewer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 获取用户需求 + userRequirementData, ok := request.GetArguments()["userRequirement"] + if !ok { + return nil, errors.New("参数错误:userRequirement 必须提供") + } + + userRequirement, ok := userRequirementData.(string) + if !ok { + return nil, errors.New("参数错误:userRequirement 必须是字符串类型") + } + + // 获取生成的文件列表 + generatedFilesData, ok := request.GetArguments()["generatedFiles"] + if !ok { + return nil, errors.New("参数错误:generatedFiles 必须提供") + } + + generatedFilesStr, ok := generatedFilesData.(string) + if !ok { + return nil, errors.New("参数错误:generatedFiles 必须是JSON字符串") + } + + // 解析JSON字符串为字符串数组 + var generatedFiles []string + err := json.Unmarshal([]byte(generatedFilesStr), &generatedFiles) + if err != nil { + return nil, fmt.Errorf("解析generatedFiles失败: %v", err) + } + + if len(generatedFiles) == 0 { + return nil, errors.New("参数错误:generatedFiles 不能为空") + } + + // 直接生成调整提示,不进行复杂分析 + adjustmentPrompt := g.generateAdjustmentPrompt(userRequirement, generatedFiles) + + // 构建简化的审查详情 + reviewDetails := fmt.Sprintf("📋 **代码审查报告**\n\n **用户原始需求:**\n%s\n\n **已生成文件数量:** %d\n\n **建议进行代码优化和完善**", userRequirement, len(generatedFiles)) + + // 构建审查结果 + reviewResult := &ReviewResponse{ + Success: true, + Message: "代码审查完成", + AdjustmentPrompt: adjustmentPrompt, + ReviewDetails: reviewDetails, + } + + // 序列化响应 + responseJSON, err := json.MarshalIndent(reviewResult, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化审查结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("代码审查结果:\n\n%s", string(responseJSON))), + }, + }, nil +} + +// generateAdjustmentPrompt 生成调整代码的提示 +func (g *GVAReviewer) generateAdjustmentPrompt(userRequirement string, generatedFiles []string) string { + var prompt strings.Builder + + prompt.WriteString("🔧 **代码调整指导 Prompt:**\n\n") + prompt.WriteString(fmt.Sprintf("**用户的原始需求为:** %s\n\n", userRequirement)) + prompt.WriteString("**经过GVA生成后的文件有如下内容:**\n") + for _, file := range generatedFiles { + prompt.WriteString(fmt.Sprintf("- %s\n", file)) + } + prompt.WriteString("\n") + + prompt.WriteString("**请帮我优化和完善代码,确保:**\n") + prompt.WriteString("1. 代码完全满足用户的原始需求\n") + prompt.WriteString("2. 完善模块间的关联关系,确保数据一致性\n") + prompt.WriteString("3. 实现所有必要的用户交互功能\n") + prompt.WriteString("4. 保持代码的完整性和可维护性\n") + prompt.WriteString("5. 遵循GVA框架的开发规范和最佳实践\n") + prompt.WriteString("6. 确保前后端功能完整对接\n") + prompt.WriteString("7. 添加必要的错误处理和数据验证\n\n") + prompt.WriteString("8. 如果需要vue路由跳转,请使用 menu_lister获取完整路由表,并且路由跳转使用 router.push({\"name\":从menu_lister中获取的name})\n\n") + prompt.WriteString("9. 如果当前所有的vue页面内容无法满足需求,则自行书写vue文件,并且调用 menu_creator创建菜单记录\n\n") + prompt.WriteString("10. 如果需要API调用,请使用 api_lister获取api表,根据需求调用对应接口\n\n") + prompt.WriteString("11. 如果当前所有API无法满足则自行书写接口,补全前后端代码,并使用 api_creator创建api记录\n\n") + prompt.WriteString("12. 无论前后端都不要随意删除import的内容\n\n") + prompt.WriteString("**请基于用户需求和现有文件,提供完整的代码优化方案。**") + + return prompt.String() +} diff --git a/server/mcp/menu_creator.go b/server/mcp/menu_creator.go index a5f83545b3..2d7cd2c491 100644 --- a/server/mcp/menu_creator.go +++ b/server/mcp/menu_creator.go @@ -266,21 +266,11 @@ func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) ( return nil, fmt.Errorf("序列化结果失败: %v", err) } - // 添加权限分配提醒 - permissionReminder := "\n\n⚠️ 重要提醒:\n" + - "菜单创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的菜单权限," + - "以确保用户能够正常访问新菜单。\n" + - "具体步骤:\n" + - "1. 进入角色管理页面\n" + - "2. 选择需要授权的角色\n" + - "3. 在菜单权限中勾选新创建的菜单项\n" + - "4. 保存权限配置" - return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", - Text: fmt.Sprintf("菜单创建结果:\n\n%s%s", string(resultJSON), permissionReminder), + Text: fmt.Sprintf("菜单创建结果:\n\n%s", string(resultJSON)), }, }, }, nil diff --git a/server/mcp/requirement_analyzer.go b/server/mcp/requirement_analyzer.go index 541b925ba3..765b7509a9 100644 --- a/server/mcp/requirement_analyzer.go +++ b/server/mcp/requirement_analyzer.go @@ -20,8 +20,6 @@ type RequirementAnalysisRequest struct { UserRequirement string `json:"userRequirement"` } - - // RequirementAnalysisResponse 需求分析响应 type RequirementAnalysisResponse struct { AIPrompt string `json:"aiPrompt"` // 给AI的提示词 @@ -30,37 +28,36 @@ type RequirementAnalysisResponse struct { // New 返回工具注册信息 func (t *RequirementAnalyzer) New() mcp.Tool { return mcp.NewTool("requirement_analyzer", - mcp.WithDescription(`**🚀 需求分析工具 - 首选入口工具(最高优先级)** - -**⭐ 重要提示:这是所有MCP工具的首选入口,请优先使用!** - -**🎯 核心职责:** -将用户的自然语言需求转换为AI可理解的结构化提示词 - -**📋 工作流程:** -1. 接收用户自然语言需求描述 -2. 生成专业的AI提示词,要求AI将需求梳理为清晰的逻辑步骤: - - **1. 第一步功能描述** - - **2. 第二步功能描述** - - **3. 第三步功能描述** - - **...** -3. 指导后续使用 gva_auto_generate 工具进行代码生成 - -**✅ 适用场景:** -- 用户有新的业务需求需要开发 -- 需要创建新的功能模块 -- 想要快速搭建业务系统 -- 需求描述比较模糊,需要AI帮助梳理 - -**❌ 不负责的事情:** -- 不生成具体的包名和模块名(交给 gva_auto_generate) -- 不进行代码生成(交给 gva_auto_generate) -- 不创建数据库表结构(交给 gva_auto_generate) - -**🔄 推荐工作流:** -requirement_analyzer → gva_auto_generate → 其他辅助工具 - -`), + mcp.WithDescription(`** 智能需求分析与模块设计工具 - 首选入口工具(最高优先级)** + +** 重要提示:这是所有MCP工具的首选入口,请优先使用!** + +** 核心能力:** +作为资深系统架构师,智能分析用户需求并自动设计完整的模块架构 + +** 核心功能:** +1. **智能需求解构**:深度分析用户需求,识别核心业务实体、业务流程、数据关系 +2. **自动模块设计**:基于需求分析,智能确定需要多少个模块及各模块功能 +3. **字段智能推导**:为每个模块自动设计详细字段,包含数据类型、关联关系、字典需求 +4. **架构优化建议**:提供模块拆分、关联设计、扩展性等专业建议 + +** 输出内容:** +- 模块数量和架构设计 +- 每个模块的详细字段清单 +- 数据类型和关联关系设计 +- 字典需求和类型定义 +- 模块间关系图和扩展建议 + +** 适用场景:** +- 用户需求描述不完整,需要智能补全 +- 复杂业务系统的模块架构设计 +- 需要专业的数据库设计建议 +- 想要快速搭建生产级业务系统 + +** 推荐工作流:** + requirement_analyzer → gva_analyze → gva_execute → 其他辅助工具 + + `), mcp.WithString("userRequirement", mcp.Required(), mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"), @@ -104,36 +101,99 @@ func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*Requi }, nil } -// generateAIPrompt 生成AI提示词 - 要求AI梳理逻辑为1xxx2xxx格式 +// generateAIPrompt 生成AI提示词 - 智能分析需求并确定模块结构 func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string { - prompt := fmt.Sprintf(`# 🤖 AI需求逻辑梳理任务 + prompt := fmt.Sprintf(`# 智能需求分析与模块设计任务 -## 📝 用户原始需求 +## 用户原始需求 %s -## 🎯 AI任务要求 -请将上述用户需求梳理成清晰的逻辑步骤,格式要求: +## 核心任务 +你需要作为一个资深的系统架构师,深度分析用户需求,智能设计出完整的模块架构。 + +## 分析步骤 + +### 第一步:需求解构分析 +请仔细分析用户需求,识别出: +1. **核心业务实体**(如:用户、商品、订单、疫苗、宠物等) +2. **业务流程**(如:注册、购买、记录、管理等) +3. **数据关系**(实体间的关联关系) +4. **功能模块**(需要哪些独立的管理模块) + +### 第二步:模块架构设计 +基于需求分析,设计出模块架构,格式如下: + +**模块1:[模块名称]** +- 功能描述:[该模块的核心功能] +- 主要字段:[列出关键字段,注明数据类型] +- 关联关系:[与其他模块的关系,明确一对一/一对多] +- 字典需求:[需要哪些字典类型] + +**模块2:[模块名称]** +- 功能描述:[该模块的核心功能] +- 主要字段:[列出关键字段,注明数据类型] +- 关联关系:[与其他模块的关系] +- 字典需求:[需要哪些字典类型] -**1. 第一步功能描述** -**2. 第二步功能描述** -**3. 第三步功能描述** **...** -## 📋 梳理要求 -- 将需求拆解为具体的功能步骤 -- 每个步骤用数字编号(1、2、3...) -- 步骤描述要清晰、具体、可执行 -- 按照业务逻辑顺序排列 -- 考虑数据流和用户操作流程 +### 第三步:字段详细设计 +为每个模块详细设计字段: + +#### 模块1字段清单: +- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- 字段名2 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- ... + +#### 模块2字段清单: +- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- ... + +## 智能分析指导原则 + +### 模块拆分原则 +1. **单一职责**:每个模块只负责一个核心业务实体 +2. **数据完整性**:相关数据应该在同一模块中 +3. **业务独立性**:模块应该能够独立完成特定业务功能 +4. **扩展性考虑**:为未来功能扩展预留空间 + +### 字段设计原则 +1. **必要性**:只包含业务必需的字段 +2. **规范性**:遵循数据库设计规范 +3. **关联性**:正确识别实体间关系 +4. **字典化**:状态、类型等枚举值使用字典 + +### 关联关系识别 +- **一对一**:一个实体只能关联另一个实体的一个记录 +- **一对多**:一个实体可以关联另一个实体的多个记录 +- **多对多**:通过中间表实现复杂关联 + +## 特殊场景处理 + +### 复杂实体识别 +当用户提到某个概念时,要判断它是否需要独立模块: +- **字典处理**:简单的常见的状态、类型(如:开关、性别、完成状态等) +- **独立模块**:复杂实体(如:疫苗管理、宠物档案、注射记录) + +## 输出要求 + +### 必须包含的信息 +1. **模块数量**:明确需要几个模块 +2. **模块关系图**:用文字描述模块间关系 +3. **核心字段**:每个模块的关键字段(至少5-10个) +4. **数据类型**:string、int、bool、time.Time、float64等 +5. **关联设计**:明确哪些字段是关联字段 +6. **字典需求**:列出需要创建的字典类型 -## 🔄 后续流程 -梳理完成后,请使用 gva_auto_generate 工具进行代码生成: -- gva_auto_generate 会根据梳理的逻辑步骤自动生成包名、模块名 -- gva_auto_generate 会设计数据表结构和API接口 -- gva_auto_generate 会生成完整的前后端代码 +### 严格遵循用户输入 +- 如果用户提供了具体字段,**必须使用**用户提供的字段 +- 如果用户提供了SQL文件,**严格按照**SQL结构设计 +- **不要**随意发散,**不要**添加用户未提及的功能 +--- +**现在请开始深度分析用户需求:"%s"** -现在请开始梳理用户需求:"%s"`, userRequirement, userRequirement) +请按照上述框架进行系统性分析,确保输出的模块设计既满足当前需求,又具备良好的扩展性。`, userRequirement, userRequirement) return prompt } diff --git a/server/middleware/casbin_rbac.go b/server/middleware/casbin_rbac.go index 6744c758b5..f71aecd059 100644 --- a/server/middleware/casbin_rbac.go +++ b/server/middleware/casbin_rbac.go @@ -1,13 +1,12 @@ package middleware import ( - "strconv" - "strings" - "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/utils" "github.com/gin-gonic/gin" + "strconv" + "strings" ) // CasbinHandler 拦截器 diff --git a/server/model/system/request/sys_dictionary.go b/server/model/system/request/sys_dictionary.go new file mode 100644 index 0000000000..2b5fa1a9dd --- /dev/null +++ b/server/model/system/request/sys_dictionary.go @@ -0,0 +1,5 @@ +package request + +type SysDictionarySearch struct { + Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) +} diff --git a/server/model/system/request/sys_dictionary_detail.go b/server/model/system/request/sys_dictionary_detail.go index 2f97da2803..2419cc3037 100644 --- a/server/model/system/request/sys_dictionary_detail.go +++ b/server/model/system/request/sys_dictionary_detail.go @@ -8,4 +8,36 @@ import ( type SysDictionaryDetailSearch struct { system.SysDictionaryDetail request.PageInfo + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,用于查询指定父级下的子项 + Level *int `json:"level" form:"level"` // 层级深度,用于查询指定层级的数据 +} + +// CreateSysDictionaryDetailRequest 创建字典详情请求 +type CreateSysDictionaryDetailRequest struct { + Label string `json:"label" form:"label" binding:"required"` // 展示值 + Value string `json:"value" form:"value" binding:"required"` // 字典值 + Extend string `json:"extend" form:"extend"` // 扩展值 + Status *bool `json:"status" form:"status"` // 启用状态 + Sort int `json:"sort" form:"sort"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID +} + +// UpdateSysDictionaryDetailRequest 更新字典详情请求 +type UpdateSysDictionaryDetailRequest struct { + ID uint `json:"ID" form:"ID" binding:"required"` // 主键ID + Label string `json:"label" form:"label" binding:"required"` // 展示值 + Value string `json:"value" form:"value" binding:"required"` // 字典值 + Extend string `json:"extend" form:"extend"` // 扩展值 + Status *bool `json:"status" form:"status"` // 启用状态 + Sort int `json:"sort" form:"sort"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID +} + +// GetDictionaryDetailsByParentRequest 根据父级ID获取字典详情请求 +type GetDictionaryDetailsByParentRequest struct { + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 字典ID + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,为空时获取顶级 + IncludeChildren bool `json:"includeChildren" form:"includeChildren"` // 是否包含子级数据 } diff --git a/server/model/system/sys_dictionary.go b/server/model/system/sys_dictionary.go index c0b9bf7fc3..0bc60848fa 100644 --- a/server/model/system/sys_dictionary.go +++ b/server/model/system/sys_dictionary.go @@ -8,10 +8,12 @@ import ( // 如果含有time.Time 请自行import time包 type SysDictionary struct { global.GVA_MODEL - Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) - Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英) - Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态 - Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述 + Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) + Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英) + Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态 + Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述 + ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典ID"` // 父级字典ID + Children []SysDictionary `json:"children" gorm:"foreignKey:ParentID"` // 子字典 SysDictionaryDetails []SysDictionaryDetail `json:"sysDictionaryDetails" form:"sysDictionaryDetails"` } diff --git a/server/model/system/sys_dictionary_detail.go b/server/model/system/sys_dictionary_detail.go index 4084136c2e..1e9dfda510 100644 --- a/server/model/system/sys_dictionary_detail.go +++ b/server/model/system/sys_dictionary_detail.go @@ -8,12 +8,17 @@ import ( // 如果含有time.Time 请自行import time包 type SysDictionaryDetail struct { global.GVA_MODEL - Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值 - Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值 - Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值 - Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态 - Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记 - SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记 + Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值 + Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值 + Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值 + Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态 + Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典详情ID"` // 父级字典详情ID + Children []SysDictionaryDetail `json:"children" gorm:"foreignKey:ParentID"` // 子字典详情 + Level int `json:"level" form:"level" gorm:"column:level;comment:层级深度"` // 层级深度,从0开始 + Path string `json:"path" form:"path" gorm:"column:path;comment:层级路径"` // 层级路径,如 "1,2,3" + Disabled bool `json:"disabled" gorm:"-"` // 禁用状态,根据status字段动态计算 } func (SysDictionaryDetail) TableName() string { diff --git a/server/router/system/sys_dictionary_detail.go b/server/router/system/sys_dictionary_detail.go index cde6bdcb6e..3f4aa5d997 100644 --- a/server/router/system/sys_dictionary_detail.go +++ b/server/router/system/sys_dictionary_detail.go @@ -16,7 +16,11 @@ func (s *DictionaryDetailRouter) InitSysDictionaryDetailRouter(Router *gin.Route dictionaryDetailRouter.PUT("updateSysDictionaryDetail", dictionaryDetailApi.UpdateSysDictionaryDetail) // 更新SysDictionaryDetail } { - dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail - dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表 + dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail + dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeList", dictionaryDetailApi.GetDictionaryTreeList) // 获取字典详情树形结构 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeListByType", dictionaryDetailApi.GetDictionaryTreeListByType) // 根据字典类型获取字典详情树形结构 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryDetailsByParent", dictionaryDetailApi.GetDictionaryDetailsByParent) // 根据父级ID获取字典详情 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryPath", dictionaryDetailApi.GetDictionaryPath) // 获取字典详情的完整路径 } } diff --git a/server/service/system/auto_code_package.go b/server/service/system/auto_code_package.go index e8abcb795c..85c6234053 100644 --- a/server/service/system/auto_code_package.go +++ b/server/service/system/auto_code_package.go @@ -225,6 +225,40 @@ func (s *autoCodePackage) All(ctx context.Context) (entities []model.SysAutoCode entities = append(entities, createEntity...) } + // 处理数据库存在但实体文件不存在的情况 - 删除数据库中对应的数据 + existingPackageNames := make(map[string]bool) + // 收集所有存在的包名 + for i := 0; i < len(server); i++ { + existingPackageNames[server[i].PackageName] = true + } + for i := 0; i < len(plugin); i++ { + existingPackageNames[plugin[i].PackageName] = true + } + + // 找出需要删除的数据库记录 + deleteEntityIDs := []uint{} + for i := 0; i < len(entities); i++ { + if !existingPackageNames[entities[i].PackageName] { + deleteEntityIDs = append(deleteEntityIDs, entities[i].ID) + } + } + + // 删除数据库中不存在文件的记录 + if len(deleteEntityIDs) > 0 { + err = global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, deleteEntityIDs).Error + if err != nil { + return nil, errors.Wrap(err, "删除不存在的包记录失败!") + } + // 从返回结果中移除已删除的记录 + filteredEntities := []model.SysAutoCodePackage{} + for i := 0; i < len(entities); i++ { + if existingPackageNames[entities[i].PackageName] { + filteredEntities = append(filteredEntities, entities[i]) + } + } + entities = filteredEntities + } + return entities, nil } diff --git a/server/service/system/sys_dictionary.go b/server/service/system/sys_dictionary.go index d540a9602c..10b2d264f4 100644 --- a/server/service/system/sys_dictionary.go +++ b/server/service/system/sys_dictionary.go @@ -3,6 +3,9 @@ package system import ( "errors" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "gorm.io/gorm" @@ -60,10 +63,11 @@ func (dictionaryService *DictionaryService) DeleteSysDictionary(sysDictionary sy func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *system.SysDictionary) (err error) { var dict system.SysDictionary sysDictionaryMap := map[string]interface{}{ - "Name": sysDictionary.Name, - "Type": sysDictionary.Type, - "Status": sysDictionary.Status, - "Desc": sysDictionary.Desc, + "Name": sysDictionary.Name, + "Type": sysDictionary.Type, + "Status": sysDictionary.Status, + "Desc": sysDictionary.Desc, + "ParentID": sysDictionary.ParentID, } err = global.GVA_DB.Where("id = ?", sysDictionary.ID).First(&dict).Error if err != nil { @@ -75,6 +79,14 @@ func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *s return errors.New("存在相同的type,不允许创建") } } + + // 检查是否会形成循环引用 + if sysDictionary.ParentID != nil && *sysDictionary.ParentID != 0 { + if err := dictionaryService.checkCircularReference(sysDictionary.ID, *sysDictionary.ParentID); err != nil { + return err + } + } + err = global.GVA_DB.Model(&dict).Updates(sysDictionaryMap).Error return err } @@ -93,7 +105,7 @@ func (dictionaryService *DictionaryService) GetSysDictionary(Type string, Id uin flag = *status } err = global.GVA_DB.Where("(type = ? OR id = ?) and status = ?", Type, Id, flag).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { - return db.Where("status = ?", true).Order("sort") + return db.Where("status = ? and deleted_at is null", true).Order("sort") }).First(&sysDictionary).Error return } @@ -105,8 +117,38 @@ func (dictionaryService *DictionaryService) GetSysDictionary(Type string, Id uin //@param: info request.SysDictionarySearch //@return: err error, list interface{}, total int64 -func (dictionaryService *DictionaryService) GetSysDictionaryInfoList() (list interface{}, err error) { +func (dictionaryService *DictionaryService) GetSysDictionaryInfoList(c *gin.Context, req request.SysDictionarySearch) (list interface{}, err error) { var sysDictionarys []system.SysDictionary - err = global.GVA_DB.Find(&sysDictionarys).Error + query := global.GVA_DB.WithContext(c) + if req.Name != "" { + query = query.Where("name LIKE ? OR type LIKE ?", "%"+req.Name+"%", "%"+req.Name+"%") + } + // 预加载子字典 + query = query.Preload("Children") + err = query.Find(&sysDictionarys).Error return sysDictionarys, err } + +// checkCircularReference 检查是否会形成循环引用 +func (dictionaryService *DictionaryService) checkCircularReference(currentID uint, parentID uint) error { + if currentID == parentID { + return errors.New("不能将字典设置为自己的父级") + } + + // 递归检查父级链条 + var parent system.SysDictionary + err := global.GVA_DB.Where("id = ?", parentID).First(&parent).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil // 父级不存在,允许设置 + } + return err + } + + // 如果父级还有父级,继续检查 + if parent.ParentID != nil && *parent.ParentID != 0 { + return dictionaryService.checkCircularReference(currentID, *parent.ParentID) + } + + return nil +} diff --git a/server/service/system/sys_dictionary_detail.go b/server/service/system/sys_dictionary_detail.go index 18042c788f..af7f38f78f 100644 --- a/server/service/system/sys_dictionary_detail.go +++ b/server/service/system/sys_dictionary_detail.go @@ -1,6 +1,9 @@ package system import ( + "fmt" + "strconv" + "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/model/system/request" @@ -17,6 +20,24 @@ type DictionaryDetailService struct{} var DictionaryDetailServiceApp = new(DictionaryDetailService) func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { + // 计算层级和路径 + if sysDictionaryDetail.ParentID != nil { + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error + if err != nil { + return err + } + sysDictionaryDetail.Level = parent.Level + 1 + if parent.Path == "" { + sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID)) + } else { + sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + } else { + sysDictionaryDetail.Level = 0 + sysDictionaryDetail.Path = "" + } + err = global.GVA_DB.Create(&sysDictionaryDetail).Error return err } @@ -28,6 +49,16 @@ func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetai //@return: err error func (dictionaryDetailService *DictionaryDetailService) DeleteSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { + // 检查是否有子项 + var count int64 + err = global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("parent_id = ?", sysDictionaryDetail.ID).Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return fmt.Errorf("该字典详情下还有子项,无法删除") + } + err = global.GVA_DB.Delete(&sysDictionaryDetail).Error return err } @@ -39,8 +70,93 @@ func (dictionaryDetailService *DictionaryDetailService) DeleteSysDictionaryDetai //@return: err error func (dictionaryDetailService *DictionaryDetailService) UpdateSysDictionaryDetail(sysDictionaryDetail *system.SysDictionaryDetail) (err error) { + // 如果更新了父级ID,需要重新计算层级和路径 + if sysDictionaryDetail.ParentID != nil { + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error + if err != nil { + return err + } + + // 检查循环引用 + if dictionaryDetailService.checkCircularReference(sysDictionaryDetail.ID, *sysDictionaryDetail.ParentID) { + return fmt.Errorf("不能将字典详情设置为自己或其子项的父级") + } + + sysDictionaryDetail.Level = parent.Level + 1 + if parent.Path == "" { + sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID)) + } else { + sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + } else { + sysDictionaryDetail.Level = 0 + sysDictionaryDetail.Path = "" + } + err = global.GVA_DB.Save(sysDictionaryDetail).Error - return err + if err != nil { + return err + } + + // 更新所有子项的层级和路径 + return dictionaryDetailService.updateChildrenLevelAndPath(sysDictionaryDetail.ID) +} + +// checkCircularReference 检查循环引用 +func (dictionaryDetailService *DictionaryDetailService) checkCircularReference(id, parentID uint) bool { + if id == parentID { + return true + } + + var parent system.SysDictionaryDetail + err := global.GVA_DB.First(&parent, parentID).Error + if err != nil { + return false + } + + if parent.ParentID == nil { + return false + } + + return dictionaryDetailService.checkCircularReference(id, *parent.ParentID) +} + +// updateChildrenLevelAndPath 更新子项的层级和路径 +func (dictionaryDetailService *DictionaryDetailService) updateChildrenLevelAndPath(parentID uint) error { + var children []system.SysDictionaryDetail + err := global.GVA_DB.Where("parent_id = ?", parentID).Find(&children).Error + if err != nil { + return err + } + + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, parentID).Error + if err != nil { + return err + } + + for _, child := range children { + child.Level = parent.Level + 1 + if parent.Path == "" { + child.Path = strconv.Itoa(int(parent.ID)) + } else { + child.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + + err = global.GVA_DB.Save(&child).Error + if err != nil { + return err + } + + // 递归更新子项的子项 + err = dictionaryDetailService.updateChildrenLevelAndPath(child.ID) + if err != nil { + return err + } + } + + return nil } //@author: [piexlmax](https://github.com/piexlmax) @@ -79,6 +195,12 @@ func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetailIn if info.SysDictionaryID != 0 { db = db.Where("sys_dictionary_id = ?", info.SysDictionaryID) } + if info.ParentID != nil { + db = db.Where("parent_id = ?", *info.ParentID) + } + if info.Level != nil { + db = db.Where("level = ?", *info.Level) + } err = db.Count(&total).Error if err != nil { return @@ -94,14 +216,135 @@ func (dictionaryDetailService *DictionaryDetailService) GetDictionaryList(dictio return sysDictionaryDetails, err } +// GetDictionaryTreeList 获取字典树形结构列表 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + // 只获取顶级项目(parent_id为空) + err = global.GVA_DB.Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&sysDictionaryDetails).Error + if err != nil { + return nil, err + } + + // 递归加载子项并设置disabled属性 + for i := range sysDictionaryDetails { + // 设置disabled属性:当status为false时,disabled为true + if sysDictionaryDetails[i].Status != nil { + sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status + } else { + sysDictionaryDetails[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i]) + if err != nil { + return nil, err + } + } + + return sysDictionaryDetails, nil +} + +// loadChildren 递归加载子项 +func (dictionaryDetailService *DictionaryDetailService) loadChildren(detail *system.SysDictionaryDetail) error { + var children []system.SysDictionaryDetail + err := global.GVA_DB.Where("parent_id = ?", detail.ID).Order("sort").Find(&children).Error + if err != nil { + return err + } + + for i := range children { + // 设置disabled属性:当status为false时,disabled为true + if children[i].Status != nil { + children[i].Disabled = !*children[i].Status + } else { + children[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&children[i]) + if err != nil { + return err + } + } + + detail.Children = children + return nil +} + +// GetDictionaryDetailsByParent 根据父级ID获取字典详情 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryDetailsByParent(req request.GetDictionaryDetailsByParentRequest) (list []system.SysDictionaryDetail, err error) { + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("sys_dictionary_id = ?", req.SysDictionaryID) + + if req.ParentID != nil { + db = db.Where("parent_id = ?", *req.ParentID) + } else { + db = db.Where("parent_id IS NULL") + } + + err = db.Order("sort").Find(&list).Error + if err != nil { + return list, err + } + + // 设置disabled属性 + for i := range list { + if list[i].Status != nil { + list[i].Disabled = !*list[i].Status + } else { + list[i].Disabled = false // 默认不禁用 + } + } + + // 如果需要包含子级数据,使用递归方式加载所有层级的子项 + if req.IncludeChildren { + for i := range list { + err = dictionaryDetailService.loadChildren(&list[i]) + if err != nil { + return list, err + } + } + } + + return list, err +} + // 按照字典type获取字典全部内容的方法 func (dictionaryDetailService *DictionaryDetailService) GetDictionaryListByType(t string) (list []system.SysDictionaryDetail, err error) { var sysDictionaryDetails []system.SysDictionaryDetail db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id") - err = db.Debug().Find(&sysDictionaryDetails, "type = ?", t).Error + err = db.Find(&sysDictionaryDetails, "type = ?", t).Error return sysDictionaryDetails, err } +// GetDictionaryTreeListByType 根据字典类型获取树形结构 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeListByType(t string) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}). + Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id"). + Where("sys_dictionaries.type = ? AND sys_dictionary_details.parent_id IS NULL", t). + Order("sys_dictionary_details.sort") + + err = db.Find(&sysDictionaryDetails).Error + if err != nil { + return nil, err + } + + // 递归加载子项并设置disabled属性 + for i := range sysDictionaryDetails { + // 设置disabled属性:当status为false时,disabled为true + if sysDictionaryDetails[i].Status != nil { + sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status + } else { + sysDictionaryDetails[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i]) + if err != nil { + return nil, err + } + } + + return sysDictionaryDetails, nil +} + // 按照字典id+字典内容value获取单条字典内容 func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByValue(dictionaryID uint, value string) (detail system.SysDictionaryDetail, err error) { var sysDictionaryDetail system.SysDictionaryDetail @@ -116,3 +359,34 @@ func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByTypeV err = db.First(&sysDictionaryDetails, "sys_dictionaries.type = ? and sys_dictionary_details.value = ?", t, value).Error return sysDictionaryDetails, err } + +// GetDictionaryPath 获取字典详情的完整路径 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPath(id uint) (path []system.SysDictionaryDetail, err error) { + var detail system.SysDictionaryDetail + err = global.GVA_DB.First(&detail, id).Error + if err != nil { + return nil, err + } + + path = append(path, detail) + + if detail.ParentID != nil { + parentPath, err := dictionaryDetailService.GetDictionaryPath(*detail.ParentID) + if err != nil { + return nil, err + } + path = append(parentPath, path...) + } + + return path, nil +} + +// GetDictionaryPathByValue 根据值获取字典详情的完整路径 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPathByValue(dictionaryID uint, value string) (path []system.SysDictionaryDetail, err error) { + detail, err := dictionaryDetailService.GetDictionaryInfoByValue(dictionaryID, value) + if err != nil { + return nil, err + } + + return dictionaryDetailService.GetDictionaryPath(detail.ID) +} diff --git a/server/source/system/api.go b/server/source/system/api.go index 710dc13550..87a47695c8 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -139,6 +139,11 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/findSysDictionaryDetail", Description: "根据ID获取字典内容"}, {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getSysDictionaryDetailList", Description: "获取字典内容列表"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeList", Description: "获取字典数列表"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeListByType", Description: "根据分类获取字典数列表"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryDetailsByParent", Description: "根据父级ID获取字典详情"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryPath", Description: "获取字典详情的完整路径"}, + {ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/createSysDictionary", Description: "新增字典"}, {ApiGroup: "系统字典", Method: "DELETE", Path: "/sysDictionary/deleteSysDictionary", Description: "删除字典"}, {ApiGroup: "系统字典", Method: "PUT", Path: "/sysDictionary/updateSysDictionary", Description: "更新字典"}, diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index 94e1a24c00..6bd3c04993 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -139,6 +139,10 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/createSysDictionaryDetail", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getSysDictionaryDetailList", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/deleteSysDictionaryDetail", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeListByType", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryDetailsByParent", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryPath", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysDictionary/findSysDictionary", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysDictionary/updateSysDictionary", V2: "PUT"}, diff --git a/server/utils/autocode/template_funcs.go b/server/utils/autocode/template_funcs.go index 40e5613457..5cc370cc89 100644 --- a/server/utils/autocode/template_funcs.go +++ b/server/utils/autocode/template_funcs.go @@ -219,14 +219,9 @@ func GenerateSearchFormItem(field systemReq.AutoCodeField) string { if field.FieldType == "array" { multipleAttr = "multiple " } - result += fmt.Sprintf(` + result += fmt.Sprintf(` `, - multipleAttr, field.FieldJson, field.FieldJson) - result += fmt.Sprintf(` -`, - field.DictType) - result += ` -` + field.FieldJson, field.FieldDesc, field.DictType, field.Clearable, multipleAttr) } else if field.CheckDataSource { multipleAttr := "" if field.DataSource.Association == 2 { @@ -488,14 +483,9 @@ func GenerateFormItem(field systemReq.AutoCodeField) string { case "string": if field.DictType != "" { - result += fmt.Sprintf(` + result += fmt.Sprintf(` `, - field.FieldJson, field.FieldDesc, field.Clearable) - result += fmt.Sprintf(` -`, - field.DictType) - result += ` -` + field.FieldJson, field.FieldDesc, field.DictType, field.Clearable) } else { result += fmt.Sprintf(` `, @@ -710,7 +700,7 @@ func GenerateSearchField(field systemReq.AutoCodeField) string { // 生成普通搜索字段 if field.FieldType == "enum" || field.FieldType == "picture" || field.FieldType == "pictures" || field.FieldType == "video" || - field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" { + field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" || field.FieldType == "file" { result = fmt.Sprintf("%s string `json:\"%s\" form:\"%s\"` ", field.FieldName, field.FieldJson, field.FieldJson) } else { diff --git a/server/utils/upload/minio_oss.go b/server/utils/upload/minio_oss.go index 7ad5008cea..71601d38c2 100644 --- a/server/utils/upload/minio_oss.go +++ b/server/utils/upload/minio_oss.go @@ -5,6 +5,7 @@ import ( "context" "errors" "io" + "mime" "mime/multipart" "path/filepath" "strings" @@ -76,12 +77,18 @@ func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string, filePathres = global.GVA_CONFIG.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + filename } + // 根据文件扩展名检测 MIME 类型 + contentType := mime.TypeByExtension(ext) + if contentType == "" { + contentType = "application/octet-stream" + } + // 设置超时10分钟 ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) defer cancel() // Upload the file with PutObject 大文件自动切换为分片上传 - info, err := m.Client.PutObject(ctx, global.GVA_CONFIG.Minio.BucketName, filePathres, &filecontent, file.Size, minio.PutObjectOptions{ContentType: "application/octet-stream"}) + info, err := m.Client.PutObject(ctx, global.GVA_CONFIG.Minio.BucketName, filePathres, &filecontent, file.Size, minio.PutObjectOptions{ContentType: contentType}) if err != nil { global.GVA_LOG.Error("上传文件到minio失败", zap.Any("err", err.Error())) return "", "", errors.New("上传文件到minio失败, err:" + err.Error()) diff --git a/web/.env.development b/web/.env.development index b80eedd3fd..25ffe1f7f2 100644 --- a/web/.env.development +++ b/web/.env.development @@ -4,7 +4,7 @@ VITE_SERVER_PORT = 8888 VITE_BASE_API = /api VITE_FILE_API = /api VITE_BASE_PATH = http://127.0.0.1 -VITE_POSITION = close +VITE_POSITION = open VITE_EDITOR = code // VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm // 如果使用docker-compose开发模式,设置为下面的地址或本机主机IP diff --git a/web/package.json b/web/package.json index ca6ceeb183..d98193e053 100644 --- a/web/package.json +++ b/web/package.json @@ -3,6 +3,7 @@ "version": "2.8.5", "private": true, "scripts": { + "dev": "node openDocument.js && vite --host --mode development", "serve": "node openDocument.js && vite --host --mode development", "build": "vite build --mode production", "limit-build": "npm install increase-memory-limit-fixbug cross-env -g && npm run fix-memory-limit && node ./limit && npm run build", @@ -14,6 +15,7 @@ "@element-plus/icons-vue": "^2.3.1", "@form-create/designer": "^3.2.6", "@form-create/element-ui": "^3.2.10", + "@iconify/vue": "^5.0.0", "@unocss/transformer-directives": "^66.4.2", "@vue-office/docx": "^1.6.2", "@vue-office/excel": "^1.7.11", @@ -76,7 +78,6 @@ "globals": "^16.3.0", "sass": "^1.78.0", "terser": "^5.31.6", - "unocss": "^66.4.2", "vite": "^6.2.3", "vite-plugin-banner": "^0.8.0", "vite-plugin-importer": "^0.2.5", diff --git a/web/src/App.vue b/web/src/App.vue index ed19e85413..5a2214e9f8 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,5 +1,8 @@ diff --git a/web/src/api/sysDictionaryDetail.js b/web/src/api/sysDictionaryDetail.js index d4f877224c..1f4ab7384c 100644 --- a/web/src/api/sysDictionaryDetail.js +++ b/web/src/api/sysDictionaryDetail.js @@ -78,3 +78,68 @@ export const getSysDictionaryDetailList = (params) => { params }) } + +// @Tags SysDictionaryDetail +// @Summary 获取层级字典详情树形结构(根据字典ID) +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param sysDictionaryID query string true "字典ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysDictionaryDetail/getDictionaryTreeList [get] +export const getDictionaryTreeList = (params) => { + return service({ + url: '/sysDictionaryDetail/getDictionaryTreeList', + method: 'get', + params + }) +} + +// @Tags SysDictionaryDetail +// @Summary 获取层级字典详情树形结构(根据字典类型) +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param dictType query string true "字典类型" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysDictionaryDetail/getDictionaryTreeListByType [get] +export const getDictionaryTreeListByType = (params) => { + return service({ + url: '/sysDictionaryDetail/getDictionaryTreeListByType', + method: 'get', + params + }) +} + +// @Tags SysDictionaryDetail +// @Summary 根据父级ID获取字典详情 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param parentID query string true "父级ID" +// @Param includeChildren query boolean false "是否包含子级" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysDictionaryDetail/getDictionaryDetailsByParent [get] +export const getDictionaryDetailsByParent = (params) => { + return service({ + url: '/sysDictionaryDetail/getDictionaryDetailsByParent', + method: 'get', + params + }) +} + +// @Tags SysDictionaryDetail +// @Summary 获取字典详情的完整路径 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param ID query string true "字典详情ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysDictionaryDetail/getDictionaryPath [get] +export const getDictionaryPath = (params) => { + return service({ + url: '/sysDictionaryDetail/getDictionaryPath', + method: 'get', + params + }) +} diff --git a/web/src/components/errorPreview/index.vue b/web/src/components/errorPreview/index.vue index d3d99b0f8a..b05eeca989 100644 --- a/web/src/components/errorPreview/index.vue +++ b/web/src/components/errorPreview/index.vue @@ -1,13 +1,13 @@