From 876017115c0f91b967e9e659e6d502f05813d3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PiexlMax=28=E5=A5=87=E6=B7=BC?= <165128580+pixelmaxQm@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:33:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=8F=91=E5=B8=832.8.9=E7=89=88=E6=9C=AC?= =?UTF-8?q?=20(#2175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ai相关调用调整 * feat: ai请求模式变更 * feat: ai自动化完善 * feat: 添加环境变量支持,条件渲染开发环境特定元素 * feat: 2.5版本插件,不再冗余进主项目,可以做到完全自动安装。 * feat: 添加插件管理功能,包括获取插件列表和删除插件的API接口 * feat: 同步测试插件的id调整 * feat: 增加插件相关初始化api * feat: 增加登录日志 * feat: 增加签发token功能 * feat: 更安全的目录清理功能,确保仅在无Go文件时删除目录 * feat: 增加skills定义能力 * feat: 优化技能管理界面,增强用户体验和视觉效果 * feat: 更新授权链接,确保用户获取最新的授权信息 * feat: 修改技能定义器名称为“Skills管理”,并重命名文件为sys_skills.go * feat: 增加全局约束的获取和保存功能,更新相关API和前端实现 * feat: 增加skills全局约束管理 * feat: 添加 vite-check-multiple-dom 插件,支持路由多根检测 (#2173) --------- Co-authored-by: piexlMax(奇淼 Co-authored-by: Azir-11 <2075125282@qq.com> Co-authored-by: 青菜白玉汤 <79054161+Azir-11@users.noreply.github.com> --- README.md | 4 +- server/api/v1/system/auto_code_plugin.go | 75 ++ server/api/v1/system/enter.go | 6 + server/api/v1/system/sys_api_token.go | 81 ++ server/api/v1/system/sys_login_log.go | 82 ++ server/api/v1/system/sys_skills.go | 149 ++++ server/api/v1/system/sys_user.go | 34 + server/initialize/ensure_tables.go | 2 + server/initialize/plugin_biz_v2.go | 4 +- server/initialize/router.go | 15 +- server/mcp/gva_analyze.go | 11 +- server/model/system/request/sys_api_token.go | 12 + server/model/system/request/sys_login_log.go | 11 + server/model/system/request/sys_skills.go | 52 ++ server/model/system/response/sys_auto_code.go | 10 + server/model/system/sys_api_token.go | 17 + server/model/system/sys_login_log.go | 16 + server/model/system/sys_skills.go | 23 + server/plugin/announcement/initialize/menu.go | 2 +- server/plugin/announcement/plugin.go | 4 + server/plugin/plugin-tool/utils/check.go | 57 +- server/plugin/register.go | 5 + server/resource/plugin/server/plugin.go.tpl | 5 + server/router/system/enter.go | 4 + server/router/system/sys_api_token.go | 19 + server/router/system/sys_auto_code.go | 3 +- server/router/system/sys_login_log.go | 23 + server/router/system/sys_skills.go | 23 + server/service/system/auto_code_llm.go | 2 +- server/service/system/auto_code_plugin.go | 227 ++++- server/service/system/enter.go | 3 + server/service/system/sys_api_token.go | 106 +++ server/service/system/sys_error.go | 9 +- server/service/system/sys_login_log.go | 53 ++ server/service/system/sys_skills.go | 512 +++++++++++ server/source/system/api.go | 24 + server/source/system/casbin.go | 24 + server/source/system/menu.go | 3 + server/utils/plugin/v2/registry.go | 27 + web/package.json | 1 + web/src/api/autoCode.js | 62 +- web/src/api/skills.js | 96 +++ web/src/api/sysApiToken.js | 25 + web/src/api/sysLoginLog.js | 33 + web/src/main.js | 5 + web/src/utils/env.js | 3 + web/src/view/dashboard/components/table.vue | 13 +- web/src/view/layout/header/tools.vue | 3 +- web/src/view/login/index.vue | 3 +- web/src/view/superAdmin/api/api.vue | 4 +- .../superAdmin/dictionary/sysDictionary.vue | 18 +- .../dictionary/sysDictionaryDetail.vue | 3 - web/src/view/systemTools/apiToken/index.vue | 299 +++++++ web/src/view/systemTools/autoCode/index.vue | 15 +- web/src/view/systemTools/autoCode/picture.vue | 12 +- .../view/systemTools/autoCodeAdmin/index.vue | 10 +- .../exportTemplate/exportTemplate.vue | 15 +- .../view/systemTools/installPlugin/index.vue | 86 +- web/src/view/systemTools/loginLog/index.vue | 180 ++++ web/src/view/systemTools/skills/index.vue | 793 ++++++++++++++++++ web/vite.config.js | 8 +- 61 files changed, 3324 insertions(+), 102 deletions(-) create mode 100644 server/api/v1/system/sys_api_token.go create mode 100644 server/api/v1/system/sys_login_log.go create mode 100644 server/api/v1/system/sys_skills.go create mode 100644 server/model/system/request/sys_api_token.go create mode 100644 server/model/system/request/sys_login_log.go create mode 100644 server/model/system/request/sys_skills.go create mode 100644 server/model/system/sys_api_token.go create mode 100644 server/model/system/sys_login_log.go create mode 100644 server/model/system/sys_skills.go create mode 100644 server/plugin/register.go create mode 100644 server/router/system/sys_api_token.go create mode 100644 server/router/system/sys_login_log.go create mode 100644 server/router/system/sys_skills.go create mode 100644 server/service/system/sys_api_token.go create mode 100644 server/service/system/sys_login_log.go create mode 100644 server/service/system/sys_skills.go create mode 100644 server/utils/plugin/v2/registry.go create mode 100644 web/src/api/skills.js create mode 100644 web/src/api/sysApiToken.js create mode 100644 web/src/api/sysLoginLog.js create mode 100644 web/src/utils/env.js create mode 100644 web/src/view/systemTools/apiToken/index.vue create mode 100644 web/src/view/systemTools/loginLog/index.vue create mode 100644 web/src/view/systemTools/skills/index.vue diff --git a/README.md b/README.md index 6eea7bbd..24e165e8 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ 3.您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行[付费支持](https://www.gin-vue-admin.com/coffee/payment.html) -4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://www.gin-vue-admin.com/empower/index.html) +4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://plugin.gin-vue-admin.com/licenseindex.html) \ @@ -381,5 +381,5 @@ fmt.Println(decodeBytes, err) ## 10. 注意事项 -请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://www.gin-vue-admin.com/empower/) +请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://plugin.gin-vue-admin.com/license) 未授权去除版权信息将依法追究法律责任 diff --git a/server/api/v1/system/auto_code_plugin.go b/server/api/v1/system/auto_code_plugin.go index 8428607f..c751b8b0 100644 --- a/server/api/v1/system/auto_code_plugin.go +++ b/server/api/v1/system/auto_code_plugin.go @@ -2,9 +2,14 @@ package system import ( "fmt" + "os" + "path/filepath" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" "github.com/gin-gonic/gin" "go.uber.org/zap" ) @@ -141,3 +146,73 @@ func (a *AutoCodePluginApi) InitDictionary(c *gin.Context) { } response.OkWithMessage("文件变更成功", c) } + +// GetPluginList +// @Tags AutoCodePlugin +// @Summary 获取插件列表 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{data=[]systemRes.PluginInfo} "获取插件列表成功" +// @Router /autoCode/getPluginList [get] +func (a *AutoCodePluginApi) GetPluginList(c *gin.Context) { + serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin") + webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin") + + serverEntries, _ := os.ReadDir(serverDir) + webEntries, _ := os.ReadDir(webDir) + + configMap := make(map[string]string) + + for _, entry := range serverEntries { + if entry.IsDir() { + configMap[entry.Name()] = "server" + } + } + + for _, entry := range webEntries { + if entry.IsDir() { + if val, ok := configMap[entry.Name()]; ok { + if val == "server" { + configMap[entry.Name()] = "full" + } + } else { + configMap[entry.Name()] = "web" + } + } + } + + var list []systemRes.PluginInfo + for k, v := range configMap { + apis, menus, dicts := utils.GetPluginData(k) + list = append(list, systemRes.PluginInfo{ + PluginName: k, + PluginType: v, + Apis: apis, + Menus: menus, + Dictionaries: dicts, + }) + } + + response.OkWithDetailed(list, "获取成功", c) +} + +// Remove +// @Tags AutoCodePlugin +// @Summary 删除插件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param pluginName query string true "插件名称" +// @Param pluginType query string true "插件类型" +// @Success 200 {object} response.Response{msg=string} "删除插件成功" +// @Router /autoCode/removePlugin [post] +func (a *AutoCodePluginApi) Remove(c *gin.Context) { + pluginName := c.Query("pluginName") + pluginType := c.Query("pluginType") + err := autoCodePluginService.Remove(pluginName, pluginType) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} diff --git a/server/api/v1/system/enter.go b/server/api/v1/system/enter.go index e6d37e2c..cde513b5 100644 --- a/server/api/v1/system/enter.go +++ b/server/api/v1/system/enter.go @@ -24,6 +24,9 @@ type ApiGroup struct { SysParamsApi SysVersionApi SysErrorApi + LoginLogApi + ApiTokenApi + SkillsApi } var ( @@ -48,4 +51,7 @@ var ( autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService + loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService + apiTokenService = service.ServiceGroupApp.SystemServiceGroup.ApiTokenService + skillsService = service.ServiceGroupApp.SystemServiceGroup.SkillsService ) diff --git a/server/api/v1/system/sys_api_token.go b/server/api/v1/system/sys_api_token.go new file mode 100644 index 00000000..14582d8e --- /dev/null +++ b/server/api/v1/system/sys_api_token.go @@ -0,0 +1,81 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + sysReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type ApiTokenApi struct{} + +// CreateApiToken 签发Token +func (s *ApiTokenApi) CreateApiToken(c *gin.Context) { + var req struct { + UserID uint `json:"userId"` + AuthorityID uint `json:"authorityId"` + Days int `json:"days"` // -1为永久, 其他为天数 + Remark string `json:"remark"` + } + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + token := system.SysApiToken{ + UserID: req.UserID, + AuthorityID: req.AuthorityID, + Remark: req.Remark, + } + + jwtStr, err := apiTokenService.CreateApiToken(token, req.Days) + if err != nil { + global.GVA_LOG.Error("签发失败!", zap.Error(err)) + response.FailWithMessage("签发失败: "+err.Error(), c) + return + } + + response.OkWithDetailed(gin.H{"token": jwtStr}, "签发成功", c) +} + +// GetApiTokenList 获取列表 +func (s *ApiTokenApi) GetApiTokenList(c *gin.Context) { + var pageInfo sysReq.SysApiTokenSearch + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := apiTokenService.GetApiTokenList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// DeleteApiToken 作废Token +func (s *ApiTokenApi) DeleteApiToken(c *gin.Context) { + var req system.SysApiToken + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiTokenService.DeleteApiToken(req.ID) + if err != nil { + global.GVA_LOG.Error("作废失败!", zap.Error(err)) + response.FailWithMessage("作废失败", c) + return + } + response.OkWithMessage("作废成功", c) +} diff --git a/server/api/v1/system/sys_login_log.go b/server/api/v1/system/sys_login_log.go new file mode 100644 index 00000000..6a6b391c --- /dev/null +++ b/server/api/v1/system/sys_login_log.go @@ -0,0 +1,82 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type LoginLogApi struct{} + +func (s *LoginLogApi) DeleteLoginLog(c *gin.Context) { + var loginLog system.SysLoginLog + err := c.ShouldBindJSON(&loginLog) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = loginLogService.DeleteLoginLog(loginLog) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +func (s *LoginLogApi) DeleteLoginLogByIds(c *gin.Context) { + var SDS request.IdsReq + err := c.ShouldBindJSON(&SDS) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = loginLogService.DeleteLoginLogByIds(SDS) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +func (s *LoginLogApi) FindLoginLog(c *gin.Context) { + var loginLog system.SysLoginLog + err := c.ShouldBindQuery(&loginLog) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + reLoginLog, err := loginLogService.GetLoginLog(loginLog.ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithDetailed(reLoginLog, "查询成功", c) +} + +func (s *LoginLogApi) GetLoginLogList(c *gin.Context) { + var pageInfo systemReq.SysLoginLogSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := loginLogService.GetLoginLogInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} diff --git a/server/api/v1/system/sys_skills.go b/server/api/v1/system/sys_skills.go new file mode 100644 index 00000000..b443ff63 --- /dev/null +++ b/server/api/v1/system/sys_skills.go @@ -0,0 +1,149 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SkillsApi struct{} + +func (s *SkillsApi) GetTools(c *gin.Context) { + data, err := skillsService.Tools(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取工具列表失败", zap.Error(err)) + response.FailWithMessage("获取工具列表失败", c) + return + } + response.OkWithDetailed(gin.H{"tools": data}, "获取成功", c) +} + +func (s *SkillsApi) GetSkillList(c *gin.Context) { + var req request.SkillToolRequest + _ = c.ShouldBindJSON(&req) + data, err := skillsService.List(c.Request.Context(), req.Tool) + if err != nil { + global.GVA_LOG.Error("获取技能列表失败", zap.Error(err)) + response.FailWithMessage("获取技能列表失败", c) + return + } + response.OkWithDetailed(gin.H{"skills": data}, "获取成功", c) +} + +func (s *SkillsApi) GetSkillDetail(c *gin.Context) { + var req request.SkillDetailRequest + _ = c.ShouldBindJSON(&req) + data, err := skillsService.Detail(c.Request.Context(), req.Tool, req.Skill) + if err != nil { + global.GVA_LOG.Error("获取技能详情失败", zap.Error(err)) + response.FailWithMessage("获取技能详情失败", c) + return + } + response.OkWithDetailed(gin.H{"detail": data}, "获取成功", c) +} + +func (s *SkillsApi) SaveSkill(c *gin.Context) { + var req request.SkillSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.Save(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存技能失败", zap.Error(err)) + response.FailWithMessage("保存技能失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) CreateScript(c *gin.Context) { + var req request.SkillScriptCreateRequest + _ = c.ShouldBindJSON(&req) + fileName, content, err := skillsService.CreateScript(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("创建脚本失败", zap.Error(err)) + response.FailWithMessage("创建脚本失败", c) + return + } + response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c) +} + +func (s *SkillsApi) GetScript(c *gin.Context) { + var req request.SkillFileRequest + _ = c.ShouldBindJSON(&req) + content, err := skillsService.GetScript(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("读取脚本失败", zap.Error(err)) + response.FailWithMessage("读取脚本失败", c) + return + } + response.OkWithDetailed(gin.H{"content": content}, "获取成功", c) +} + +func (s *SkillsApi) SaveScript(c *gin.Context) { + var req request.SkillFileSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.SaveScript(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存脚本失败", zap.Error(err)) + response.FailWithMessage("保存脚本失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) CreateResource(c *gin.Context) { + var req request.SkillResourceCreateRequest + _ = c.ShouldBindJSON(&req) + fileName, content, err := skillsService.CreateResource(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("创建资源失败", zap.Error(err)) + response.FailWithMessage("创建资源失败", c) + return + } + response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c) +} + +func (s *SkillsApi) GetResource(c *gin.Context) { + var req request.SkillFileRequest + _ = c.ShouldBindJSON(&req) + content, err := skillsService.GetResource(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("读取资源失败", zap.Error(err)) + response.FailWithMessage("读取资源失败", c) + return + } + response.OkWithDetailed(gin.H{"content": content}, "获取成功", c) +} + +func (s *SkillsApi) SaveResource(c *gin.Context) { + var req request.SkillFileSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.SaveResource(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存资源失败", zap.Error(err)) + response.FailWithMessage("保存资源失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) GetGlobalConstraint(c *gin.Context) { + var req request.SkillToolRequest + _ = c.ShouldBindJSON(&req) + content, exists, err := skillsService.GetGlobalConstraint(c.Request.Context(), req.Tool) + if err != nil { + global.GVA_LOG.Error("读取全局约束失败", zap.Error(err)) + response.FailWithMessage("读取全局约束失败", c) + return + } + response.OkWithDetailed(gin.H{"content": content, "exists": exists}, "获取成功", c) +} + +func (s *SkillsApi) SaveGlobalConstraint(c *gin.Context) { + var req request.SkillGlobalConstraintSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.SaveGlobalConstraint(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存全局约束失败", zap.Error(err)) + response.FailWithMessage("保存全局约束失败", c) + return + } + response.OkWithMessage("保存成功", c) +} diff --git a/server/api/v1/system/sys_user.go b/server/api/v1/system/sys_user.go index ae6ab022..12b28493 100644 --- a/server/api/v1/system/sys_user.go +++ b/server/api/v1/system/sys_user.go @@ -51,6 +51,14 @@ func (b *BaseApi) Login(c *gin.Context) { // 验证码次数+1 global.BlackCache.Increment(key, 1) response.FailWithMessage("验证码错误", c) + // 记录登录失败日志 + loginLogService.CreateLoginLog(system.SysLoginLog{ + Username: l.Username, + Ip: c.ClientIP(), + Agent: c.Request.UserAgent(), + Status: false, + ErrorMessage: "验证码错误", + }) return } @@ -61,6 +69,14 @@ func (b *BaseApi) Login(c *gin.Context) { // 验证码次数+1 global.BlackCache.Increment(key, 1) response.FailWithMessage("用户名不存在或者密码错误", c) + // 记录登录失败日志 + loginLogService.CreateLoginLog(system.SysLoginLog{ + Username: l.Username, + Ip: c.ClientIP(), + Agent: c.Request.UserAgent(), + Status: false, + ErrorMessage: "用户名不存在或者密码错误", + }) return } if user.Enable != 1 { @@ -68,6 +84,15 @@ func (b *BaseApi) Login(c *gin.Context) { // 验证码次数+1 global.BlackCache.Increment(key, 1) response.FailWithMessage("用户被禁止登录", c) + // 记录登录失败日志 + loginLogService.CreateLoginLog(system.SysLoginLog{ + Username: l.Username, + Ip: c.ClientIP(), + Agent: c.Request.UserAgent(), + Status: false, + ErrorMessage: "用户被禁止登录", + UserID: user.ID, + }) return } b.TokenNext(c, *user) @@ -81,6 +106,15 @@ func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { response.FailWithMessage("获取token失败", c) return } + // 记录登录成功日志 + loginLogService.CreateLoginLog(system.SysLoginLog{ + Username: user.Username, + Ip: c.ClientIP(), + Agent: c.Request.UserAgent(), + Status: true, + UserID: user.ID, + ErrorMessage: "登录成功", + }) if !global.GVA_CONFIG.System.UseMultipoint { utils.SetToken(c, token, int(claims.RegisteredClaims.ExpiresAt.Unix()-time.Now().Unix())) response.OkWithDetailed(systemRes.LoginResponse{ diff --git a/server/initialize/ensure_tables.go b/server/initialize/ensure_tables.go index 3a6c4540..d9e6cccb 100644 --- a/server/initialize/ensure_tables.go +++ b/server/initialize/ensure_tables.go @@ -55,6 +55,8 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error sysModel.SysParams{}, sysModel.SysVersion{}, sysModel.SysError{}, + sysModel.SysLoginLog{}, + sysModel.SysApiToken{}, adapter.CasbinRule{}, example.ExaFile{}, diff --git a/server/initialize/plugin_biz_v2.go b/server/initialize/plugin_biz_v2.go index 9d13bbe0..bde58fa1 100644 --- a/server/initialize/plugin_biz_v2.go +++ b/server/initialize/plugin_biz_v2.go @@ -1,7 +1,7 @@ package initialize import ( - "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement" + _ "github.com/flipped-aurora/gin-vue-admin/server/plugin" "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2" "github.com/gin-gonic/gin" ) @@ -12,5 +12,5 @@ func PluginInitV2(group *gin.Engine, plugins ...plugin.Plugin) { } } func bizPluginV2(engine *gin.Engine) { - PluginInitV2(engine, announcement.Plugin) + PluginInitV2(engine, plugin.Registered()...) } diff --git a/server/initialize/router.go b/server/initialize/router.go index e5334d60..83438a9a 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -34,12 +34,12 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) { // 初始化总路由 func Routers() *gin.Engine { - Router := gin.New() - // 使用自定义的 Recovery 中间件,记录 panic 并入库 - Router.Use(middleware.GinRecovery(true)) - if gin.Mode() == gin.DebugMode { - Router.Use(gin.Logger()) - } + Router := gin.New() + // 使用自定义的 Recovery 中间件,记录 panic 并入库 + Router.Use(middleware.GinRecovery(true)) + if gin.Mode() == gin.DebugMode { + Router.Use(gin.Logger()) + } if !global.GVA_CONFIG.MCP.Separate { @@ -109,6 +109,9 @@ func Routers() *gin.Engine { systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板 systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理 systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志 + systemRouter.InitLoginLogRouter(PrivateGroup) // 登录日志 + systemRouter.InitApiTokenRouter(PrivateGroup) // apiToken签发 + systemRouter.InitSkillsRouter(PrivateGroup) // Skills 定义器 exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类 diff --git a/server/mcp/gva_analyze.go b/server/mcp/gva_analyze.go index 3611b543..1e974714 100644 --- a/server/mcp/gva_analyze.go +++ b/server/mcp/gva_analyze.go @@ -349,7 +349,16 @@ func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error { return err // 其他错误 } - return os.RemoveAll(dirPath) + // 检查目录中是否包含go文件 + noGoFiles, err := g.hasGoFilesRecursive(dirPath) + if err != nil { + return err + } + // hasGoFilesRecursive 返回 false 表示发现了 go 文件 + if noGoFiles { + return os.RemoveAll(dirPath) + } + return nil } // cleanupRelatedApiAndMenus 清理相关的API和菜单记录 diff --git a/server/model/system/request/sys_api_token.go b/server/model/system/request/sys_api_token.go new file mode 100644 index 00000000..e9e1c020 --- /dev/null +++ b/server/model/system/request/sys_api_token.go @@ -0,0 +1,12 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysApiTokenSearch struct { + system.SysApiToken + request.PageInfo + Status *bool `json:"status" form:"status"` +} diff --git a/server/model/system/request/sys_login_log.go b/server/model/system/request/sys_login_log.go new file mode 100644 index 00000000..48d7256f --- /dev/null +++ b/server/model/system/request/sys_login_log.go @@ -0,0 +1,11 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysLoginLogSearch struct { + system.SysLoginLog + request.PageInfo +} diff --git a/server/model/system/request/sys_skills.go b/server/model/system/request/sys_skills.go new file mode 100644 index 00000000..b61d91e2 --- /dev/null +++ b/server/model/system/request/sys_skills.go @@ -0,0 +1,52 @@ +package request + +import "github.com/flipped-aurora/gin-vue-admin/server/model/system" + +type SkillToolRequest struct { + Tool string `json:"tool"` +} + +type SkillDetailRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` +} + +type SkillSaveRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + Meta system.SkillMeta `json:"meta"` + Markdown string `json:"markdown"` + SyncTools []string `json:"syncTools"` +} + +type SkillScriptCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` + ScriptType string `json:"scriptType"` +} + +type SkillResourceCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillFileRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillFileSaveRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` + Content string `json:"content"` +} + +type SkillGlobalConstraintSaveRequest struct { + Tool string `json:"tool"` + Content string `json:"content"` + SyncTools []string `json:"syncTools"` +} diff --git a/server/model/system/response/sys_auto_code.go b/server/model/system/response/sys_auto_code.go index 9e99bde3..e1f1d8b0 100644 --- a/server/model/system/response/sys_auto_code.go +++ b/server/model/system/response/sys_auto_code.go @@ -1,5 +1,7 @@ package response +import "github.com/flipped-aurora/gin-vue-admin/server/model/system" + type Db struct { Database string `json:"database" gorm:"column:database"` } @@ -15,3 +17,11 @@ type Column struct { ColumnComment string `json:"columnComment" gorm:"column:column_comment"` PrimaryKey bool `json:"primaryKey" gorm:"column:primary_key"` } + +type PluginInfo struct { + PluginName string `json:"pluginName"` + PluginType string `json:"pluginType"` // web, server, full + Apis []system.SysApi `json:"apis"` + Menus []system.SysBaseMenu `json:"menus"` + Dictionaries []system.SysDictionary `json:"dictionaries"` +} diff --git a/server/model/system/sys_api_token.go b/server/model/system/sys_api_token.go new file mode 100644 index 00000000..2bfbde8a --- /dev/null +++ b/server/model/system/sys_api_token.go @@ -0,0 +1,17 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "time" +) + +type SysApiToken struct { + global.GVA_MODEL + UserID uint `json:"userId" gorm:"comment:用户ID"` + User SysUser `json:"user" gorm:"foreignKey:UserID;"` + AuthorityID uint `json:"authorityId" gorm:"comment:角色ID"` + Token string `json:"token" gorm:"type:text;comment:Token"` + Status bool `json:"status" gorm:"default:true;comment:状态"` // true有效 false无效 + ExpiresAt time.Time `json:"expiresAt" gorm:"comment:过期时间"` + Remark string `json:"remark" gorm:"comment:备注"` +} diff --git a/server/model/system/sys_login_log.go b/server/model/system/sys_login_log.go new file mode 100644 index 00000000..b1fd08be --- /dev/null +++ b/server/model/system/sys_login_log.go @@ -0,0 +1,16 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type SysLoginLog struct { + global.GVA_MODEL + Username string `json:"username" gorm:"column:username;comment:用户名"` + Ip string `json:"ip" gorm:"column:ip;comment:请求ip"` + Status bool `json:"status" gorm:"column:status;comment:登录状态"` + ErrorMessage string `json:"errorMessage" gorm:"column:error_message;comment:错误信息"` + Agent string `json:"agent" gorm:"column:agent;comment:代理"` + UserID uint `json:"userId" gorm:"column:user_id;comment:用户id"` + User SysUser `json:"user" gorm:"foreignKey:UserID"` +} diff --git a/server/model/system/sys_skills.go b/server/model/system/sys_skills.go new file mode 100644 index 00000000..76f6c341 --- /dev/null +++ b/server/model/system/sys_skills.go @@ -0,0 +1,23 @@ +package system + +type SkillMeta struct { + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + AllowedTools string `json:"allowedTools" yaml:"allowed-tools,omitempty"` + Context string `json:"context" yaml:"context,omitempty"` + Agent string `json:"agent" yaml:"agent,omitempty"` +} + +type SkillDetail struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + Meta SkillMeta `json:"meta"` + Markdown string `json:"markdown"` + Scripts []string `json:"scripts"` + Resources []string `json:"resources"` +} + +type SkillTool struct { + Key string `json:"key"` + Label string `json:"label"` +} diff --git a/server/plugin/announcement/initialize/menu.go b/server/plugin/announcement/initialize/menu.go index 40aff2b5..58144bc4 100644 --- a/server/plugin/announcement/initialize/menu.go +++ b/server/plugin/announcement/initialize/menu.go @@ -9,7 +9,7 @@ import ( func Menu(ctx context.Context) { entities := []model.SysBaseMenu{ { - ParentId: 24, + ParentId: 9, Path: "anInfo", Name: "anInfo", Hidden: false, diff --git a/server/plugin/announcement/plugin.go b/server/plugin/announcement/plugin.go index 5c2532d9..68e2464b 100644 --- a/server/plugin/announcement/plugin.go +++ b/server/plugin/announcement/plugin.go @@ -13,6 +13,10 @@ var Plugin = new(plugin) type plugin struct{} +func init() { + interfaces.Register(Plugin) +} + func (p *plugin) Register(group *gin.Engine) { ctx := context.Background() // 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key diff --git a/server/plugin/plugin-tool/utils/check.go b/server/plugin/plugin-tool/utils/check.go index c9aedeae..70631d77 100644 --- a/server/plugin/plugin-tool/utils/check.go +++ b/server/plugin/plugin-tool/utils/check.go @@ -4,12 +4,47 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" "gorm.io/gorm" + "path/filepath" + "runtime" + "strings" + "sync" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) +var ( + ApiMap = make(map[string][]system.SysApi) + MenuMap = make(map[string][]system.SysBaseMenu) + DictMap = make(map[string][]system.SysDictionary) + rw sync.Mutex +) + +func getPluginName() string { + _, file, _, ok := runtime.Caller(2) + pluginName := "" + if ok { + file = filepath.ToSlash(file) + const key = "server/plugin/" + if idx := strings.Index(file, key); idx != -1 { + remain := file[idx+len(key):] + parts := strings.Split(remain, "/") + if len(parts) > 0 { + pluginName = parts[0] + } + } + } + return pluginName +} + func RegisterApis(apis ...system.SysApi) { + name := getPluginName() + if name != "" { + rw.Lock() + ApiMap[name] = apis + rw.Unlock() + } + err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { for _, api := range apis { err := tx.Model(system.SysApi{}).Where("path = ? AND method = ? AND api_group = ? ", api.Path, api.Method, api.ApiGroup).FirstOrCreate(&api).Error @@ -26,6 +61,13 @@ func RegisterApis(apis ...system.SysApi) { } func RegisterMenus(menus ...system.SysBaseMenu) { + name := getPluginName() + if name != "" { + rw.Lock() + MenuMap[name] = menus + rw.Unlock() + } + parentMenu := menus[0] otherMenus := menus[1:] err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { @@ -43,7 +85,6 @@ func RegisterMenus(menus ...system.SysBaseMenu) { return errors.Wrap(err, "注册菜单失败") } } - return nil }) if err != nil { @@ -53,6 +94,13 @@ func RegisterMenus(menus ...system.SysBaseMenu) { } func RegisterDictionaries(dictionaries ...system.SysDictionary) { + name := getPluginName() + if name != "" { + rw.Lock() + DictMap[name] = dictionaries + rw.Unlock() + } + err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { for _, dict := range dictionaries { details := dict.SysDictionaryDetails @@ -81,3 +129,10 @@ func RegisterDictionaries(dictionaries ...system.SysDictionary) { func Pointer[T any](in T) *T { return &in } + +func GetPluginData(pluginName string) ([]system.SysApi, []system.SysBaseMenu, []system.SysDictionary) { + rw.Lock() + defer rw.Unlock() + return ApiMap[pluginName], MenuMap[pluginName], DictMap[pluginName] +} + diff --git a/server/plugin/register.go b/server/plugin/register.go new file mode 100644 index 00000000..11399d07 --- /dev/null +++ b/server/plugin/register.go @@ -0,0 +1,5 @@ +package plugin + +import ( + _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement" +) diff --git a/server/resource/plugin/server/plugin.go.tpl b/server/resource/plugin/server/plugin.go.tpl index e718e569..43ed0569 100644 --- a/server/resource/plugin/server/plugin.go.tpl +++ b/server/resource/plugin/server/plugin.go.tpl @@ -13,6 +13,11 @@ var Plugin = new(plugin) type plugin struct{} +func init() { + interfaces.Register(Plugin) +} + + // 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key并添加如下方法 // initialize.Viper() // 安装插件时候自动注册的api数据请到下方法.Api方法中实现并添加如下方法 diff --git a/server/router/system/enter.go b/server/router/system/enter.go index 970f6630..d379c2ab 100644 --- a/server/router/system/enter.go +++ b/server/router/system/enter.go @@ -21,6 +21,9 @@ type RouterGroup struct { SysParamsRouter SysVersionRouter SysErrorRouter + LoginLogRouter + ApiTokenRouter + SkillsRouter } var ( @@ -45,4 +48,5 @@ var ( exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi sysErrorApi = api.ApiGroupApp.SystemApiGroup.SysErrorApi + skillsApi = api.ApiGroupApp.SystemApiGroup.SkillsApi ) diff --git a/server/router/system/sys_api_token.go b/server/router/system/sys_api_token.go new file mode 100644 index 00000000..ebedeb06 --- /dev/null +++ b/server/router/system/sys_api_token.go @@ -0,0 +1,19 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type ApiTokenRouter struct{} + +func (s *ApiTokenRouter) InitApiTokenRouter(Router *gin.RouterGroup) { + apiTokenRouter := Router.Group("sysApiToken").Use(middleware.OperationRecord()) + apiTokenApi := v1.ApiGroupApp.SystemApiGroup.ApiTokenApi + { + apiTokenRouter.POST("createApiToken", apiTokenApi.CreateApiToken) // 签发Token + apiTokenRouter.POST("getApiTokenList", apiTokenApi.GetApiTokenList) // 获取列表 + apiTokenRouter.POST("deleteApiToken", apiTokenApi.DeleteApiToken) // 作废Token + } +} diff --git a/server/router/system/sys_auto_code.go b/server/router/system/sys_auto_code.go index acb49d26..261196d4 100644 --- a/server/router/system/sys_auto_code.go +++ b/server/router/system/sys_auto_code.go @@ -35,7 +35,8 @@ func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPubli { autoCodeRouter.POST("pubPlug", autoCodePluginApi.Packaged) // 打包插件 autoCodeRouter.POST("installPlugin", autoCodePluginApi.Install) // 自动安装插件 - + autoCodeRouter.POST("removePlugin", autoCodePluginApi.Remove) // 自动删除插件 + autoCodeRouter.GET("getPluginList", autoCodePluginApi.GetPluginList) // 获取插件列表 } { publicAutoCodeRouter.POST("llmAuto", autoCodeApi.LLMAuto) diff --git a/server/router/system/sys_login_log.go b/server/router/system/sys_login_log.go new file mode 100644 index 00000000..3ef672e0 --- /dev/null +++ b/server/router/system/sys_login_log.go @@ -0,0 +1,23 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type LoginLogRouter struct{} + +func (s *LoginLogRouter) InitLoginLogRouter(Router *gin.RouterGroup) { + loginLogRouter := Router.Group("sysLoginLog").Use(middleware.OperationRecord()) + loginLogRouterWithoutRecord := Router.Group("sysLoginLog") + sysLoginLogApi := v1.ApiGroupApp.SystemApiGroup.LoginLogApi + { + loginLogRouter.DELETE("deleteLoginLog", sysLoginLogApi.DeleteLoginLog) // 删除登录日志 + loginLogRouter.DELETE("deleteLoginLogByIds", sysLoginLogApi.DeleteLoginLogByIds) // 批量删除登录日志 + } + { + loginLogRouterWithoutRecord.GET("findLoginLog", sysLoginLogApi.FindLoginLog) // 根据ID获取登录日志(详情) + loginLogRouterWithoutRecord.GET("getLoginLogList", sysLoginLogApi.GetLoginLogList) // 获取登录日志列表 + } +} diff --git a/server/router/system/sys_skills.go b/server/router/system/sys_skills.go new file mode 100644 index 00000000..f631f62e --- /dev/null +++ b/server/router/system/sys_skills.go @@ -0,0 +1,23 @@ +package system + +import "github.com/gin-gonic/gin" + +type SkillsRouter struct{} + +func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) { + skillsRouter := Router.Group("skills") + { + skillsRouter.GET("getTools", skillsApi.GetTools) + skillsRouter.POST("getSkillList", skillsApi.GetSkillList) + skillsRouter.POST("getSkillDetail", skillsApi.GetSkillDetail) + skillsRouter.POST("saveSkill", skillsApi.SaveSkill) + skillsRouter.POST("createScript", skillsApi.CreateScript) + skillsRouter.POST("getScript", skillsApi.GetScript) + skillsRouter.POST("saveScript", skillsApi.SaveScript) + skillsRouter.POST("createResource", skillsApi.CreateResource) + skillsRouter.POST("getResource", skillsApi.GetResource) + skillsRouter.POST("saveResource", skillsApi.SaveResource) + skillsRouter.POST("getGlobalConstraint", skillsApi.GetGlobalConstraint) + skillsRouter.POST("saveGlobalConstraint", skillsApi.SaveGlobalConstraint) + } +} diff --git a/server/service/system/auto_code_llm.go b/server/service/system/auto_code_llm.go index 76b4fb4b..83c370f8 100644 --- a/server/service/system/auto_code_llm.go +++ b/server/service/system/auto_code_llm.go @@ -22,7 +22,7 @@ func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (inte // 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换 mode := fmt.Sprintf("%v", llm["mode"]) // 统一转字符串,避免 nil 造成路径异常 - path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", mode)) + path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", mode) res, err := request.HttpRequest( path, diff --git a/server/service/system/auto_code_plugin.go b/server/service/system/auto_code_plugin.go index efeef1c1..be3dff68 100644 --- a/server/service/system/auto_code_plugin.go +++ b/server/service/system/auto_code_plugin.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + goast "go/ast" "go/parser" "go/printer" "go/token" @@ -16,8 +17,9 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + pluginUtils "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" "github.com/flipped-aurora/gin-vue-admin/server/utils" - "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + ast "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" "github.com/mholt/archives" cp "github.com/otiai10/copy" "github.com/pkg/errors" @@ -71,6 +73,8 @@ func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, e var serverIndex = -1 webPlugin := "" serverPlugin := "" + serverPackage := "" + serverRootName := "" for i := range paths { paths[i] = filepath.ToSlash(paths[i]) @@ -80,8 +84,16 @@ func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, e if ln < 4 { continue } - if pathArr[2]+"/"+pathArr[3] == `server/plugin` && len(serverPlugin) == 0 { - serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3]) + if pathArr[2]+"/"+pathArr[3] == `server/plugin` { + if len(serverPlugin) == 0 { + serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3]) + } + if serverRootName == "" && ln > 1 && pathArr[1] != "" { + serverRootName = pathArr[1] + } + if ln > 4 && serverPackage == "" && pathArr[4] != "" { + serverPackage = pathArr[4] + } } if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 { webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3]) @@ -93,10 +105,17 @@ func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, e } if len(serverPlugin) != 0 { + if serverPackage == "" { + serverPackage = serverRootName + } err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server) if err != nil { return webIndex, serverIndex, err } + err = ensurePluginRegisterImport(serverPackage) + if err != nil { + return webIndex, serverIndex, err + } } if len(webPlugin) != 0 { @@ -127,6 +146,64 @@ func installation(path string, formPath string, toPath string) error { return cp.Copy(form, to, cp.Options{Skip: skipMacSpecialDocument}) } +func ensurePluginRegisterImport(packageName string) error { + module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module) + if module == "" { + return errors.New("autocode module is empty") + } + if packageName == "" { + return errors.New("plugin package is empty") + } + + registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go") + src, err := os.ReadFile(registerPath) + if err != nil { + return err + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments) + if err != nil { + return err + } + + importPath := fmt.Sprintf("%s/plugin/%s", module, packageName) + if ast.CheckImport(astFile, importPath) { + return nil + } + + importSpec := &goast.ImportSpec{ + Name: goast.NewIdent("_"), + Path: &goast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", importPath)}, + } + var importDecl *goast.GenDecl + for _, decl := range astFile.Decls { + genDecl, ok := decl.(*goast.GenDecl) + if !ok { + continue + } + if genDecl.Tok == token.IMPORT { + importDecl = genDecl + break + } + } + if importDecl == nil { + astFile.Decls = append([]goast.Decl{ + &goast.GenDecl{ + Tok: token.IMPORT, + Specs: []goast.Spec{importSpec}, + }, + }, astFile.Decls...) + } else { + importDecl.Specs = append(importDecl.Specs, importSpec) + } + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + return os.WriteFile(registerPath, bf.Bytes(), 0666) +} + func filterFile(paths []string) []string { np := make([]string, 0, len(paths)) for _, path := range paths { @@ -289,3 +366,147 @@ func (s *autoCodePlugin) InitDictionary(dictInfo request.InitDictionary) (err er os.WriteFile(dictPath, bf.Bytes(), 0666) return nil } + +func (s *autoCodePlugin) Remove(pluginName string, pluginType string) (err error) { + // 1. 删除前端代码 + if pluginType == "web" || pluginType == "full" { + webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", pluginName) + err = os.RemoveAll(webDir) + if err != nil { + return errors.Wrap(err, "删除前端插件目录失败") + } + } + + // 2. 删除后端代码 + if pluginType == "server" || pluginType == "full" { + serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", pluginName) + err = os.RemoveAll(serverDir) + if err != nil { + return errors.Wrap(err, "删除后端插件目录失败") + } + + // 移除注册 + removePluginRegisterImport(pluginName) + } + + // 通过utils 获取 api 菜单 字典 + apis, menus, dicts := pluginUtils.GetPluginData(pluginName) + + // 3. 删除菜单 (递归删除) + if len(menus) > 0 { + for _, menu := range menus { + var dbMenu system.SysBaseMenu + if err := global.GVA_DB.Where("name = ?", menu.Name).First(&dbMenu).Error; err == nil { + // 获取该菜单及其所有子菜单的ID + var menuIds []int + GetMenuIds(dbMenu, &menuIds) + // 逆序删除,先删除子菜单 + for i := len(menuIds) - 1; i >= 0; i-- { + err := BaseMenuServiceApp.DeleteBaseMenu(menuIds[i]) + if err != nil { + zap.L().Error("删除菜单失败", zap.Int("id", menuIds[i]), zap.Error(err)) + } + } + } + } + } + + // 4. 删除API + if len(apis) > 0 { + for _, api := range apis { + var dbApi system.SysApi + if err := global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&dbApi).Error; err == nil { + err := ApiServiceApp.DeleteApi(dbApi) + if err != nil { + zap.L().Error("删除API失败", zap.String("path", api.Path), zap.Error(err)) + } + } + } + } + + // 5. 删除字典 + if len(dicts) > 0 { + for _, dict := range dicts { + var dbDict system.SysDictionary + if err := global.GVA_DB.Where("type = ?", dict.Type).First(&dbDict).Error; err == nil { + err := DictionaryServiceApp.DeleteSysDictionary(dbDict) + if err != nil { + zap.L().Error("删除字典失败", zap.String("type", dict.Type), zap.Error(err)) + } + } + } + } + + return nil +} + +func GetMenuIds(menu system.SysBaseMenu, ids *[]int) { + *ids = append(*ids, int(menu.ID)) + var children []system.SysBaseMenu + global.GVA_DB.Where("parent_id = ?", menu.ID).Find(&children) + for _, child := range children { + // 先递归收集子菜单 + GetMenuIds(child, ids) + } +} + +func removePluginRegisterImport(packageName string) error { + module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module) + if module == "" { + return errors.New("autocode module is empty") + } + if packageName == "" { + return errors.New("plugin package is empty") + } + + registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go") + src, err := os.ReadFile(registerPath) + if err != nil { + return err + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments) + if err != nil { + return err + } + + importPath := fmt.Sprintf("%s/plugin/%s", module, packageName) + importLit := fmt.Sprintf("%q", importPath) + + // 移除 import + var newDecls []goast.Decl + for _, decl := range astFile.Decls { + genDecl, ok := decl.(*goast.GenDecl) + if !ok { + newDecls = append(newDecls, decl) + continue + } + if genDecl.Tok == token.IMPORT { + var newSpecs []goast.Spec + for _, spec := range genDecl.Specs { + importSpec, ok := spec.(*goast.ImportSpec) + if !ok { + newSpecs = append(newSpecs, spec) + continue + } + if importSpec.Path.Value != importLit { + newSpecs = append(newSpecs, spec) + } + } + // 如果还有其他import,保留该 decl + if len(newSpecs) > 0 { + genDecl.Specs = newSpecs + newDecls = append(newDecls, genDecl) + } + } else { + newDecls = append(newDecls, decl) + } + } + astFile.Decls = newDecls + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + return os.WriteFile(registerPath, bf.Bytes(), 0666) +} diff --git a/server/service/system/enter.go b/server/service/system/enter.go index f09123cf..6d68bb7b 100644 --- a/server/service/system/enter.go +++ b/server/service/system/enter.go @@ -18,9 +18,12 @@ type ServiceGroup struct { SysExportTemplateService SysParamsService SysVersionService + SkillsService AutoCodePlugin autoCodePlugin AutoCodePackage autoCodePackage AutoCodeHistory autoCodeHistory AutoCodeTemplate autoCodeTemplate SysErrorService + LoginLogService + ApiTokenService } diff --git a/server/service/system/sys_api_token.go b/server/service/system/sys_api_token.go new file mode 100644 index 00000000..b2ebdde9 --- /dev/null +++ b/server/service/system/sys_api_token.go @@ -0,0 +1,106 @@ +package system + +import ( + "errors" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + sysReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/golang-jwt/jwt/v5" + "time" +) + +type ApiTokenService struct{} + +func (apiVersion *ApiTokenService) CreateApiToken(apiToken system.SysApiToken, days int) (string, error) { + var user system.SysUser + if err := global.GVA_DB.Where("id = ?", apiToken.UserID).First(&user).Error; err != nil { + return "", errors.New("用户不存在") + } + + hasAuth := false + for _, auth := range user.Authorities { + if auth.AuthorityId == apiToken.AuthorityID { + hasAuth = true + break + } + } + if !hasAuth && user.AuthorityId != apiToken.AuthorityID { + return "", errors.New("用户不具备该角色权限") + } + + j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} // 唯一不同的部分是过期时间 + + expireTime := time.Duration(days) * 24 * time.Hour + if days == -1 { + expireTime = 100 * 365 * 24 * time.Hour + } + + bf, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + + claims := sysReq.CustomClaims{ + BaseClaims: sysReq.BaseClaims{ + UUID: user.UUID, + ID: user.ID, + Username: user.Username, + NickName: user.NickName, + AuthorityId: apiToken.AuthorityID, + }, + BufferTime: int64(bf / time.Second), // 缓冲时间 + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"GVA"}, + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireTime)), + Issuer: global.GVA_CONFIG.JWT.Issuer, + }, + } + + token, err := j.CreateToken(claims) + if err != nil { + return "", err + } + + apiToken.Token = token + apiToken.Status = true + apiToken.ExpiresAt = time.Now().Add(expireTime) + err = global.GVA_DB.Create(&apiToken).Error + return token, err +} + +func (apiVersion *ApiTokenService) GetApiTokenList(info sysReq.SysApiTokenSearch) (list []system.SysApiToken, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&system.SysApiToken{}) + + db = db.Preload("User") + + if info.UserID != 0 { + db = db.Where("user_id = ?", info.UserID) + } + if info.Status != nil { + db = db.Where("status = ?", *info.Status) + } + + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&list).Error + return list, total, err +} + +func (apiVersion *ApiTokenService) DeleteApiToken(id uint) error { + var apiToken system.SysApiToken + err := global.GVA_DB.First(&apiToken, id).Error + if err != nil { + return err + } + + jwtService := JwtService{} + err = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: apiToken.Token}) + if err != nil { + return err + } + + return global.GVA_DB.Model(&apiToken).Update("status", false).Error +} diff --git a/server/service/system/sys_error.go b/server/service/system/sys_error.go index e8a1a355..065b6241 100644 --- a/server/service/system/sys_error.go +++ b/server/service/system/sys_error.go @@ -106,16 +106,15 @@ func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context, } llmReq := common.JSONMap{ - "mode": "solution", - "command": "solution", - "info": info, - "form": form, + "mode": "solution", + "info": info, + "form": form, } // 调用服务层 LLMAuto,忽略错误但尽量写入方案 var solution string if data, err := (&AutoCodeService{}).LLMAuto(context.Background(), llmReq); err == nil { - solution = fmt.Sprintf("%v", data) + solution = fmt.Sprintf("%v", data.(map[string]interface{})["text"]) _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error } else { // 即使生成失败也标记为完成,避免任务卡住 diff --git a/server/service/system/sys_login_log.go b/server/service/system/sys_login_log.go new file mode 100644 index 00000000..79fee93c --- /dev/null +++ b/server/service/system/sys_login_log.go @@ -0,0 +1,53 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +type LoginLogService struct{} + +var LoginLogServiceApp = new(LoginLogService) + +func (loginLogService *LoginLogService) CreateLoginLog(loginLog system.SysLoginLog) (err error) { + err = global.GVA_DB.Create(&loginLog).Error + return err +} + +func (loginLogService *LoginLogService) DeleteLoginLogByIds(ids request.IdsReq) (err error) { + err = global.GVA_DB.Delete(&[]system.SysLoginLog{}, "id in (?)", ids.Ids).Error + return err +} + +func (loginLogService *LoginLogService) DeleteLoginLog(loginLog system.SysLoginLog) (err error) { + err = global.GVA_DB.Delete(&loginLog).Error + return err +} + +func (loginLogService *LoginLogService) GetLoginLog(id uint) (loginLog system.SysLoginLog, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&loginLog).Error + return +} + +func (loginLogService *LoginLogService) GetLoginLogInfoList(info systemReq.SysLoginLogSearch) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysLoginLog{}) + var loginLogs []system.SysLoginLog + // 如果有条件搜索 下方会自动创建搜索语句 + if info.Username != "" { + db = db.Where("username LIKE ?", "%"+info.Username+"%") + } + if info.Status != false { + db = db.Where("status = ?", info.Status) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("id desc").Preload("User").Find(&loginLogs).Error + return loginLogs, total, err +} diff --git a/server/service/system/sys_skills.go b/server/service/system/sys_skills.go new file mode 100644 index 00000000..51b79c65 --- /dev/null +++ b/server/service/system/sys_skills.go @@ -0,0 +1,512 @@ +package system + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "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" + "gopkg.in/yaml.v3" +) + +const ( + skillFileName = "SKILL.md" + globalConstraintFileName = "README.md" +) + +var skillToolOrder = []string{"copilot", "claude", "cursor", "trae", "codex"} + +var skillToolDirs = map[string]string{ + "copilot": ".aone_copilot", + "claude": ".claude", + "trae": ".trae", + "codex": ".codex", + "cursor": ".cursor", +} + +var skillToolLabels = map[string]string{ + "copilot": "Copilot", + "claude": "Claude", + "trae": "Trae", + "codex": "Codex", + "cursor": "Cursor", +} + +const defaultSkillMarkdown = "## 技能用途\n请在这里描述技能的目标、适用场景与限制条件。\n\n## 输入\n- 请补充输入格式与示例。\n\n## 输出\n- 请补充输出格式与示例。\n\n## 关键步骤\n1. 第一步\n2. 第二步\n\n## 示例\n在此补充一到两个典型示例。\n" + +const defaultResourceMarkdown = "# 资源说明\n请在这里补充资源内容。\n" + +const defaultGlobalConstraintMarkdown = "# 全局约束\n请在这里补充该工具的统一约束与使用规范。\n" + +type SkillsService struct{} + +func (s *SkillsService) Tools(_ context.Context) ([]system.SkillTool, error) { + tools := make([]system.SkillTool, 0, len(skillToolOrder)) + for _, key := range skillToolOrder { + if _, err := s.toolSkillsDir(key); err != nil { + return nil, err + } + tools = append(tools, system.SkillTool{Key: key, Label: skillToolLabels[key]}) + } + return tools, nil +} + +func (s *SkillsService) List(_ context.Context, tool string) ([]string, error) { + skillsDir, err := s.toolSkillsDir(tool) + if err != nil { + return nil, err + } + entries, err := os.ReadDir(skillsDir) + if err != nil { + return nil, err + } + var skills []string + for _, entry := range entries { + if entry.IsDir() { + skills = append(skills, entry.Name()) + } + } + sort.Strings(skills) + return skills, nil +} + +func (s *SkillsService) Detail(_ context.Context, tool, skill string) (system.SkillDetail, error) { + var detail system.SkillDetail + if !isSafeName(skill) { + return detail, errors.New("技能名称不合法") + } + detail.Tool = tool + detail.Skill = skill + + skillDir, err := s.skillDir(tool, skill) + if err != nil { + return detail, err + } + + skillFilePath := filepath.Join(skillDir, skillFileName) + content, err := os.ReadFile(skillFilePath) + if err != nil { + if !os.IsNotExist(err) { + return detail, err + } + detail.Meta = system.SkillMeta{Name: skill} + detail.Markdown = defaultSkillMarkdown + } else { + meta, body, parseErr := parseSkillContent(string(content)) + if parseErr != nil { + meta = system.SkillMeta{Name: skill} + body = string(content) + } + if meta.Name == "" { + meta.Name = skill + } + detail.Meta = meta + detail.Markdown = body + } + + detail.Scripts = listFiles(filepath.Join(skillDir, "scripts")) + detail.Resources = listFiles(filepath.Join(skillDir, "resources")) + return detail, nil +} + +func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) error { + if !isSafeName(req.Skill) { + return errors.New("技能名称不合法") + } + skillDir, err := s.ensureSkillDir(req.Tool, req.Skill) + if err != nil { + return err + } + if req.Meta.Name == "" { + req.Meta.Name = req.Skill + } + content, err := buildSkillContent(req.Meta, req.Markdown) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(skillDir, skillFileName), []byte(content), 0644); err != nil { + return err + } + + if len(req.SyncTools) > 0 { + for _, tool := range req.SyncTools { + if tool == req.Tool { + continue + } + targetDir, err := s.ensureSkillDir(tool, req.Skill) + if err != nil { + return err + } + if err := copySkillDir(skillDir, targetDir); err != nil { + return err + } + } + } + return nil +} + +func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) { + if !isSafeName(req.Skill) { + return "", "", errors.New("技能名称不合法") + } + fileName, lang, err := buildScriptFileName(req.FileName, req.ScriptType) + if err != nil { + return "", "", err + } + if lang == "" { + return "", "", errors.New("脚本类型不支持") + } + skillDir, err := s.ensureSkillDir(req.Tool, req.Skill) + if err != nil { + return "", "", err + } + filePath := filepath.Join(skillDir, "scripts", fileName) + if _, err := os.Stat(filePath); err == nil { + return "", "", errors.New("脚本已存在") + } + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return "", "", err + } + content := scriptTemplate(lang) + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return "", "", err + } + return fileName, content, nil +} + +func (s *SkillsService) GetScript(_ context.Context, req request.SkillFileRequest) (string, error) { + return s.readSkillFile(req.Tool, req.Skill, "scripts", req.FileName) +} + +func (s *SkillsService) SaveScript(_ context.Context, req request.SkillFileSaveRequest) error { + return s.writeSkillFile(req.Tool, req.Skill, "scripts", req.FileName, req.Content) +} + +func (s *SkillsService) CreateResource(_ context.Context, req request.SkillResourceCreateRequest) (string, string, error) { + if !isSafeName(req.Skill) { + return "", "", errors.New("技能名称不合法") + } + fileName, err := buildResourceFileName(req.FileName) + if err != nil { + return "", "", err + } + skillDir, err := s.ensureSkillDir(req.Tool, req.Skill) + if err != nil { + return "", "", err + } + filePath := filepath.Join(skillDir, "resources", fileName) + if _, err := os.Stat(filePath); err == nil { + return "", "", errors.New("资源已存在") + } + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return "", "", err + } + content := defaultResourceMarkdown + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return "", "", err + } + return fileName, content, nil +} + +func (s *SkillsService) GetResource(_ context.Context, req request.SkillFileRequest) (string, error) { + return s.readSkillFile(req.Tool, req.Skill, "resources", req.FileName) +} + +func (s *SkillsService) SaveResource(_ context.Context, req request.SkillFileSaveRequest) error { + return s.writeSkillFile(req.Tool, req.Skill, "resources", req.FileName, req.Content) +} + +func (s *SkillsService) GetGlobalConstraint(_ context.Context, tool string) (string, bool, error) { + skillsDir, err := s.toolSkillsDir(tool) + if err != nil { + return "", false, err + } + filePath := filepath.Join(skillsDir, globalConstraintFileName) + content, err := os.ReadFile(filePath) + if err != nil { + if os.IsNotExist(err) { + return defaultGlobalConstraintMarkdown, false, nil + } + return "", false, err + } + return string(content), true, nil +} + +func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.SkillGlobalConstraintSaveRequest) error { + if strings.TrimSpace(req.Tool) == "" { + return errors.New("工具类型不能为空") + } + writeConstraint := func(tool, content string) error { + skillsDir, err := s.toolSkillsDir(tool) + if err != nil { + return err + } + filePath := filepath.Join(skillsDir, globalConstraintFileName) + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return err + } + return os.WriteFile(filePath, []byte(content), 0644) + } + if err := writeConstraint(req.Tool, req.Content); err != nil { + return err + } + if len(req.SyncTools) == 0 { + return nil + } + for _, tool := range req.SyncTools { + if tool == "" || tool == req.Tool { + continue + } + if err := writeConstraint(tool, req.Content); err != nil { + return err + } + } + return nil +} + +func (s *SkillsService) toolSkillsDir(tool string) (string, error) { + toolDir, ok := skillToolDirs[tool] + if !ok { + return "", errors.New("工具类型不支持") + } + root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root) + if root == "" { + root = "." + } + skillsDir := filepath.Join(root, toolDir, "skills") + if err := os.MkdirAll(skillsDir, os.ModePerm); err != nil { + return "", err + } + return skillsDir, nil +} + +func (s *SkillsService) skillDir(tool, skill string) (string, error) { + skillsDir, err := s.toolSkillsDir(tool) + if err != nil { + return "", err + } + return filepath.Join(skillsDir, skill), nil +} + +func (s *SkillsService) ensureSkillDir(tool, skill string) (string, error) { + if !isSafeName(skill) { + return "", errors.New("技能名称不合法") + } + skillDir, err := s.skillDir(tool, skill) + if err != nil { + return "", err + } + if err := os.MkdirAll(skillDir, os.ModePerm); err != nil { + return "", err + } + return skillDir, nil +} + +func (s *SkillsService) readSkillFile(tool, skill, subDir, fileName string) (string, error) { + if !isSafeName(skill) { + return "", errors.New("技能名称不合法") + } + if !isSafeFileName(fileName) { + return "", errors.New("文件名不合法") + } + skillDir, err := s.skillDir(tool, skill) + if err != nil { + return "", err + } + filePath := filepath.Join(skillDir, subDir, fileName) + content, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + return string(content), nil +} + +func (s *SkillsService) writeSkillFile(tool, skill, subDir, fileName, content string) error { + if !isSafeName(skill) { + return errors.New("技能名称不合法") + } + if !isSafeFileName(fileName) { + return errors.New("文件名不合法") + } + skillDir, err := s.ensureSkillDir(tool, skill) + if err != nil { + return err + } + filePath := filepath.Join(skillDir, subDir, fileName) + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return err + } + return os.WriteFile(filePath, []byte(content), 0644) +} + +func parseSkillContent(content string) (system.SkillMeta, string, error) { + clean := strings.TrimPrefix(content, "\ufeff") + lines := strings.Split(clean, "\n") + if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" { + return system.SkillMeta{}, clean, nil + } + end := -1 + for i := 1; i < len(lines); i++ { + if strings.TrimSpace(lines[i]) == "---" { + end = i + break + } + } + if end == -1 { + return system.SkillMeta{}, clean, nil + } + yamlText := strings.Join(lines[1:end], "\n") + body := strings.Join(lines[end+1:], "\n") + var meta system.SkillMeta + if err := yaml.Unmarshal([]byte(yamlText), &meta); err != nil { + return system.SkillMeta{}, body, err + } + return meta, body, nil +} + +func buildSkillContent(meta system.SkillMeta, markdown string) (string, error) { + if meta.Name == "" { + return "", errors.New("name不能为空") + } + data, err := yaml.Marshal(meta) + if err != nil { + return "", err + } + yamlText := strings.TrimRight(string(data), "\n") + body := strings.TrimLeft(markdown, "\n") + if body != "" { + body = body + "\n" + } + return fmt.Sprintf("---\n%s\n---\n%s", yamlText, body), nil +} + +func listFiles(dir string) []string { + entries, err := os.ReadDir(dir) + if err != nil { + return []string{} + } + files := make([]string, 0, len(entries)) + for _, entry := range entries { + if entry.Type().IsRegular() { + files = append(files, entry.Name()) + } + } + sort.Strings(files) + return files +} + +func isSafeName(name string) bool { + if strings.TrimSpace(name) == "" { + return false + } + if strings.Contains(name, "..") { + return false + } + if strings.ContainsAny(name, "/\\") { + return false + } + return name == filepath.Base(name) +} + +func isSafeFileName(name string) bool { + if strings.TrimSpace(name) == "" { + return false + } + if strings.Contains(name, "..") { + return false + } + if strings.ContainsAny(name, "/\\") { + return false + } + return name == filepath.Base(name) +} + +func buildScriptFileName(fileName, scriptType string) (string, string, error) { + clean := strings.TrimSpace(fileName) + if clean == "" { + return "", "", errors.New("文件名不能为空") + } + if !isSafeFileName(clean) { + return "", "", errors.New("文件名不合法") + } + base := strings.TrimSuffix(clean, filepath.Ext(clean)) + if base == "" { + return "", "", errors.New("文件名不合法") + } + + switch strings.ToLower(scriptType) { + case "py", "python": + return base + ".py", "python", nil + case "js", "javascript", "script": + return base + ".js", "javascript", nil + case "sh", "shell", "bash": + return base + ".sh", "sh", nil + default: + return "", "", errors.New("脚本类型不支持") + } +} + +func buildResourceFileName(fileName string) (string, error) { + clean := strings.TrimSpace(fileName) + if clean == "" { + return "", errors.New("文件名不能为空") + } + if !isSafeFileName(clean) { + return "", errors.New("文件名不合法") + } + base := strings.TrimSuffix(clean, filepath.Ext(clean)) + if base == "" { + return "", errors.New("文件名不合法") + } + return base + ".md", nil +} + +func scriptTemplate(lang string) string { + switch lang { + case "python": + return "# -*- coding: utf-8 -*-\n# TODO: 在这里实现脚本逻辑\n" + case "javascript": + return "// TODO: 在这里实现脚本逻辑\n" + case "sh": + return "#!/usr/bin/env bash\nset -euo pipefail\n\n# TODO: 在这里实现脚本逻辑\n" + default: + return "" + } +} + +func copySkillDir(src, dst string) error { + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + target := filepath.Join(dst, rel) + if d.IsDir() { + return os.MkdirAll(target, os.ModePerm) + } + if !d.Type().IsRegular() { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil { + return err + } + return os.WriteFile(target, data, 0644) + }) +} diff --git a/server/source/system/api.go b/server/source/system/api.go index 863847bd..3632840e 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -46,6 +46,15 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { entities := []sysModel.SysApi{ {ApiGroup: "jwt", Method: "POST", Path: "/jwt/jsonInBlacklist", Description: "jwt加入黑名单(退出,必选)"}, + {ApiGroup: "登录日志", Method: "DELETE", Path: "/sysLoginLog/deleteLoginLog", Description: "删除登录日志"}, + {ApiGroup: "登录日志", Method: "DELETE", Path: "/sysLoginLog/deleteLoginLogByIds", Description: "批量删除登录日志"}, + {ApiGroup: "登录日志", Method: "GET", Path: "/sysLoginLog/findLoginLog", Description: "根据ID获取登录日志"}, + {ApiGroup: "登录日志", Method: "GET", Path: "/sysLoginLog/getLoginLogList", Description: "获取登录日志列表"}, + + {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/createApiToken", Description: "签发API Token"}, + {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/getApiTokenList", Description: "获取API Token列表"}, + {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/deleteApiToken", Description: "作废API Token"}, + {ApiGroup: "系统用户", Method: "DELETE", Path: "/user/deleteUser", Description: "删除用户"}, {ApiGroup: "系统用户", Method: "POST", Path: "/user/admin_register", Description: "用户注册"}, {ApiGroup: "系统用户", Method: "POST", Path: "/user/getUserList", Description: "获取用户列表"}, @@ -105,6 +114,19 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "系统服务", Method: "POST", Path: "/system/getSystemConfig", Description: "获取配置文件内容"}, {ApiGroup: "系统服务", Method: "POST", Path: "/system/setSystemConfig", Description: "设置配置文件内容"}, + {ApiGroup: "skills", Method: "GET", Path: "/skills/getTools", Description: "获取技能工具列表"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillList", Description: "获取技能列表"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillDetail", Description: "获取技能详情"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveSkill", Description: "保存技能定义"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/createScript", Description: "创建技能脚本"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getScript", Description: "读取技能脚本"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveScript", Description: "保存技能脚本"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/createResource", Description: "创建技能资源"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getResource", Description: "读取技能资源"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveResource", Description: "保存技能资源"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getGlobalConstraint", Description: "读取全局约束"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveGlobalConstraint", Description: "保存全局约束"}, + {ApiGroup: "客户", Method: "PUT", Path: "/customer/customer", Description: "更新客户"}, {ApiGroup: "客户", Method: "POST", Path: "/customer/customer", Description: "创建客户"}, {ApiGroup: "客户", Method: "DELETE", Path: "/customer/customer", Description: "删除客户"}, @@ -118,6 +140,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getColumn", Description: "获取所选table的所有字段"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/installPlugin", Description: "安装插件"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/pubPlug", Description: "打包插件"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/removePlugin", Description: "卸载插件"}, + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getPluginList", Description: "获取已安装插件"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcp", Description: "自动生成 MCP Tool 模板"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpTest", Description: "MCP Tool 测试"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpList", Description: "获取 MCP ToolList"}, diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index a7e49d75..0cdf22ec 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -47,6 +47,15 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error entities := []adapter.CasbinRule{ {Ptype: "p", V0: "888", V1: "/user/admin_register", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysLoginLog/deleteLoginLog", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysLoginLog/deleteLoginLogByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysLoginLog/findLoginLog", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysLoginLog/getLoginLogList", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/sysApiToken/createApiToken", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysApiToken/getApiTokenList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysApiToken/deleteApiToken", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/createApi", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/api/getApiList", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/api/getApiById", V2: "POST"}, @@ -107,6 +116,19 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/system/setSystemConfig", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/system/getServerInfo", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getTools", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/skills/getSkillList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getSkillDetail", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveSkill", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/createScript", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getScript", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveScript", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/createResource", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getResource", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveResource", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getGlobalConstraint", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveGlobalConstraint", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "PUT"}, {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "POST"}, @@ -129,6 +151,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/autoCode/createPlug", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/installPlugin", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/pubPlug", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/removePlugin", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getPluginList", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/mcp", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/mcpTest", V2: "POST"}, diff --git a/server/source/system/menu.go b/server/source/system/menu.go index 078716bb..66b133a3 100644 --- a/server/source/system/menu.go +++ b/server/source/system/menu.go @@ -96,9 +96,12 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "loginLog", Name: "loginLog", Component: "view/systemTools/loginLog/index.vue", Sort: 5, Meta: Meta{Title: "登录日志", Icon: "monitor"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "apiToken", Name: "apiToken", Component: "view/systemTools/apiToken/index.vue", Sort: 6, Meta: Meta{Title: "API Token", Icon: "key"}}, {MenuLevel: 1, Hidden: true, ParentId: menuNameMap["systemTools"], Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "skills", Name: "skills", Component: "view/systemTools/skills/index.vue", Sort: 6, Meta: Meta{Title: "Skills管理", Icon: "document"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools测试", Icon: "partly-cloudy"}}, diff --git a/server/utils/plugin/v2/registry.go b/server/utils/plugin/v2/registry.go new file mode 100644 index 00000000..4ec5fcea --- /dev/null +++ b/server/utils/plugin/v2/registry.go @@ -0,0 +1,27 @@ +package plugin + +import "sync" + +var ( + registryMu sync.RWMutex + registry []Plugin +) + +// Register records a plugin for auto initialization. +func Register(p Plugin) { + if p == nil { + return + } + registryMu.Lock() + registry = append(registry, p) + registryMu.Unlock() +} + +// Registered returns a snapshot of all registered plugins. +func Registered() []Plugin { + registryMu.RLock() + defer registryMu.RUnlock() + out := make([]Plugin, len(registry)) + copy(out, registry) + return out +} diff --git a/web/package.json b/web/package.json index 5531dce3..d31bf569 100644 --- a/web/package.json +++ b/web/package.json @@ -79,6 +79,7 @@ "sass": "^1.78.0", "terser": "^5.31.6", "vite": "^6.2.3", + "vite-check-multiple-dom": "0.2.1", "vite-plugin-banner": "^0.8.0", "vite-plugin-importer": "^0.2.5", "vite-plugin-vue-devtools": "^7.0.16" diff --git a/web/src/api/autoCode.js b/web/src/api/autoCode.js index 789e92c8..8d540d34 100644 --- a/web/src/api/autoCode.js +++ b/web/src/api/autoCode.js @@ -143,7 +143,7 @@ export const llmAuto = (data) => { return service({ url: '/autoCode/llmAuto', method: 'post', - data: { ...data, mode: 'ai' }, + data: { ...data }, timeout: 1000 * 60 * 10, loadingOption: { lock: true, @@ -153,36 +153,6 @@ export const llmAuto = (data) => { }) } -export const butler = (data) => { - return service({ - url: '/autoCode/llmAuto', - method: 'post', - data: { ...data, mode: 'butler' }, - timeout: 1000 * 60 * 10 - }) -} - -export const eye = (data) => { - return service({ - url: '/autoCode/llmAuto', - method: 'post', - data: { ...data, mode: 'eye' }, - timeout: 1000 * 60 * 10 - }) -} - - -export const createWeb = (data) => { - return service({ - url: '/autoCode/llmAuto', - method: 'post', - data: { ...data, mode: 'painter' }, - timeout: 1000 * 60 * 10 - }) -} - - - export const addFunc = (data) => { return service({ url: '/autoCode/addFunc', @@ -240,3 +210,33 @@ export const mcpTest = (data) => { data }) } + +// @Tags SysApi +// @Summary 获取插件列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /autoCode/getPluginList [get] +export const getPluginList = (params) => { + return service({ + url: '/autoCode/getPluginList', + method: 'get', + params + }) +} + +// @Tags SysApi +// @Summary 删除插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /autoCode/removePlugin [post] +export const removePlugin = (params) => { + return service({ + url: '/autoCode/removePlugin', + method: 'post', + params + }) +} diff --git a/web/src/api/skills.js b/web/src/api/skills.js new file mode 100644 index 00000000..0a9dbb21 --- /dev/null +++ b/web/src/api/skills.js @@ -0,0 +1,96 @@ +import service from '@/utils/request' + +export const getSkillTools = () => { + return service({ + url: '/skills/getTools', + method: 'get' + }) +} + +export const getSkillList = (data) => { + return service({ + url: '/skills/getSkillList', + method: 'post', + data + }) +} + +export const getSkillDetail = (data) => { + return service({ + url: '/skills/getSkillDetail', + method: 'post', + data + }) +} + +export const saveSkill = (data) => { + return service({ + url: '/skills/saveSkill', + method: 'post', + data + }) +} + +export const createSkillScript = (data) => { + return service({ + url: '/skills/createScript', + method: 'post', + data + }) +} + +export const getSkillScript = (data) => { + return service({ + url: '/skills/getScript', + method: 'post', + data + }) +} + +export const saveSkillScript = (data) => { + return service({ + url: '/skills/saveScript', + method: 'post', + data + }) +} + +export const createSkillResource = (data) => { + return service({ + url: '/skills/createResource', + method: 'post', + data + }) +} + +export const getSkillResource = (data) => { + return service({ + url: '/skills/getResource', + method: 'post', + data + }) +} + +export const saveSkillResource = (data) => { + return service({ + url: '/skills/saveResource', + method: 'post', + data + }) +} + +export const getGlobalConstraint = (data) => { + return service({ + url: '/skills/getGlobalConstraint', + method: 'post', + data + }) +} + +export const saveGlobalConstraint = (data) => { + return service({ + url: '/skills/saveGlobalConstraint', + method: 'post', + data + }) +} diff --git a/web/src/api/sysApiToken.js b/web/src/api/sysApiToken.js new file mode 100644 index 00000000..f95c714a --- /dev/null +++ b/web/src/api/sysApiToken.js @@ -0,0 +1,25 @@ +import service from '@/utils/request' + +export const createApiToken = (data) => { + return service({ + url: '/sysApiToken/createApiToken', + method: 'post', + data + }) +} + +export const getApiTokenList = (data) => { + return service({ + url: '/sysApiToken/getApiTokenList', + method: 'post', + data + }) +} + +export const deleteApiToken = (data) => { + return service({ + url: '/sysApiToken/deleteApiToken', + method: 'post', + data + }) +} diff --git a/web/src/api/sysLoginLog.js b/web/src/api/sysLoginLog.js new file mode 100644 index 00000000..4c96de20 --- /dev/null +++ b/web/src/api/sysLoginLog.js @@ -0,0 +1,33 @@ +import service from '@/utils/request' + +export const deleteLoginLog = (data) => { + return service({ + url: '/sysLoginLog/deleteLoginLog', + method: 'delete', + data + }) +} + +export const deleteLoginLogByIds = (data) => { + return service({ + url: '/sysLoginLog/deleteLoginLogByIds', + method: 'delete', + data + }) +} + +export const getLoginLogList = (params) => { + return service({ + url: '/sysLoginLog/getLoginLogList', + method: 'get', + params + }) +} + +export const findLoginLog = (params) => { + return service({ + url: '/sysLoginLog/findLoginLog', + method: 'get', + params + }) +} diff --git a/web/src/main.js b/web/src/main.js index 5855b570..e5b5cc28 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -3,6 +3,7 @@ import 'element-plus/theme-chalk/dark/css-vars.css' import 'uno.css' import { createApp } from 'vue' import ElementPlus from 'element-plus' +import { setupVueRootValidator } from 'vite-check-multiple-dom/client'; import 'element-plus/dist/index.css' // 引入gin-vue-admin前端初始化相关内容 @@ -21,6 +22,10 @@ const app = createApp(App) app.config.productionTip = false +setupVueRootValidator(app, { + lang: 'zh' + }) + app .use(run) .use(ElementPlus) diff --git a/web/src/utils/env.js b/web/src/utils/env.js new file mode 100644 index 00000000..7053375c --- /dev/null +++ b/web/src/utils/env.js @@ -0,0 +1,3 @@ +export const isDev = import.meta.env.DEV; + +export const isProd = import.meta.env.PROD; diff --git a/web/src/view/dashboard/components/table.vue b/web/src/view/dashboard/components/table.vue index 262f11a7..05f2c370 100644 --- a/web/src/view/dashboard/components/table.vue +++ b/web/src/view/dashboard/components/table.vue @@ -10,12 +10,23 @@ + + diff --git a/web/src/view/systemTools/autoCode/index.vue b/web/src/view/systemTools/autoCode/index.vue index a9c4f5c4..f6b24426 100644 --- a/web/src/view/systemTools/autoCode/index.vue +++ b/web/src/view/systemTools/autoCode/index.vue @@ -822,7 +822,7 @@ preview, getMeta, getPackageApi, - llmAuto, butler, eye + llmAuto } from '@/api/autoCode' import { getDict } from '@/utils/dictionary' import { ref, watch, toRaw, onMounted, nextTick } from 'vue' @@ -860,9 +860,9 @@ const reader = new FileReader(); reader.onload =async (e) => { const base64String = e.target.result; - const res = await eye({ picture: base64String,command: 'eye' }) + const res = await llmAuto({ _file_path: base64String,mode:"eye" }) if (res.code === 0) { - prompt.value = res.data + prompt.value = res.data.text llmAutoFunc() } }; @@ -893,9 +893,9 @@ reader.onload = async (e) => { const base64String = e.target.result; - const res = await eye({ picture: base64String,command: 'eye' }) + const res = await llmAuto({ _file_path: base64String,mode:'eye' }) if (res.code === 0) { - prompt.value = res.data + prompt.value = res.data.text llmAutoFunc() } }; @@ -933,11 +933,12 @@ } const res = await llmAuto({ - prompt: flag ? '结构体名称为' + form.value.structName : prompt.value + prompt: flag ? '结构体名称为' + form.value.structName : prompt.value, + mode: "ai" }) if (res.code === 0) { form.value.fields = [] - const json = JSON.parse(res.data) + const json = JSON.parse(res.data.text) json.fields?.forEach((item) => { item.fieldName = toUpperCase(item.fieldName) }) diff --git a/web/src/view/systemTools/autoCode/picture.vue b/web/src/view/systemTools/autoCode/picture.vue index 4863d362..56a3b1c6 100644 --- a/web/src/view/systemTools/autoCode/picture.vue +++ b/web/src/view/systemTools/autoCode/picture.vue @@ -1,7 +1,7 @@ + + diff --git a/web/src/view/systemTools/skills/index.vue b/web/src/view/systemTools/skills/index.vue new file mode 100644 index 00000000..17bb8df5 --- /dev/null +++ b/web/src/view/systemTools/skills/index.vue @@ -0,0 +1,793 @@ + + + diff --git a/web/vite.config.js b/web/vite.config.js index f9b6a5e4..f8874401 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -8,6 +8,7 @@ import vuePlugin from '@vitejs/plugin-vue' import vueDevTools from 'vite-plugin-vue-devtools' import VueFilePathPlugin from './vitePlugin/componentName/index.js' import { svgBuilder } from 'vite-auto-import-svg' +import vueRootValidator from 'vite-check-multiple-dom'; import { AddSecret } from './vitePlugin/secret' import UnoCSS from '@unocss/vite' @@ -79,7 +80,7 @@ export default ({ mode }) => { rewrite: (path) => path.replace(new RegExp('^' + process.env.VITE_BASE_API), '') }, - "/plugin": { + "/plugin": { // 需要代理的路径 例如 '/api' target: `https://plugin.gin-vue-admin.com/api/`, // 代理到 目标路径 changeOrigin: true, @@ -118,10 +119,11 @@ export default ({ mode }) => { ] }), vuePlugin(), - svgBuilder(['./src/plugin/','./src/assets/icons/'],base, outDir,'assets', NODE_ENV), + svgBuilder(['./src/plugin/', './src/assets/icons/'], base, outDir, 'assets', NODE_ENV), [Banner(`\n Build based on gin-vue-admin \n Time : ${timestamp}`)], VueFilePathPlugin('./src/pathInfo.json'), - UnoCSS() + UnoCSS(), + vueRootValidator() ] } return config