feat:发布2.8.9版本 (#2175)
* 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(奇淼 <qimiaojiangjizhao@gmail.com> Co-authored-by: Azir-11 <2075125282@qq.com> Co-authored-by: 青菜白玉汤 <79054161+Azir-11@users.noreply.github.com>
This commit is contained in:
parent
755373468c
commit
876017115c
|
|
@ -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)
|
||||
\
|
||||
<img src="https://qmplusimg.henrongyi.top/openSource/login.jpg" width="1000">
|
||||
|
||||
|
|
@ -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)
|
||||
未授权去除版权信息将依法追究法律责任
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
|
|
|
|||
|
|
@ -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()...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) // 文件上传下载分类
|
||||
|
|
|
|||
|
|
@ -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和菜单记录
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:备注"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import (
|
|||
func Menu(ctx context.Context) {
|
||||
entities := []model.SysBaseMenu{
|
||||
{
|
||||
ParentId: 24,
|
||||
ParentId: 9,
|
||||
Path: "anInfo",
|
||||
Name: "anInfo",
|
||||
Hidden: false,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
_ "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement"
|
||||
)
|
||||
|
|
@ -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方法中实现并添加如下方法
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) // 获取登录日志列表
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ type ServiceGroup struct {
|
|||
SysExportTemplateService
|
||||
SysParamsService
|
||||
SysVersionService
|
||||
SkillsService
|
||||
AutoCodePlugin autoCodePlugin
|
||||
AutoCodePackage autoCodePackage
|
||||
AutoCodeHistory autoCodeHistory
|
||||
AutoCodeTemplate autoCodeTemplate
|
||||
SysErrorService
|
||||
LoginLogService
|
||||
ApiTokenService
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
// 即使生成失败也标记为完成,避免任务卡住
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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"}},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export const isDev = import.meta.env.DEV;
|
||||
|
||||
export const isProd = import.meta.env.PROD;
|
||||
|
|
@ -10,12 +10,23 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Commits } from '@/api/github'
|
||||
import { formatTimeToStr } from '@/utils/date'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const service = axios.create()
|
||||
|
||||
const tableData = ref([])
|
||||
|
||||
const Commits =(page) => {
|
||||
return service({
|
||||
url:
|
||||
'https://api.github.com/repos/flipped-aurora/gin-vue-admin/commits?page=' +
|
||||
page,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
const loadCommits = async () => {
|
||||
const { data } = await Commits(1)
|
||||
tableData.value = data.slice(0, 5).map((item, index) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="flex items-center mx-4 gap-4">
|
||||
<el-tooltip class="" effect="dark" content="视频教程" placement="bottom">
|
||||
<el-tooltip v-if="isDev" class="" effect="dark" content="视频教程" placement="bottom">
|
||||
<el-dropdown @command="toDoc">
|
||||
<span class="w-8 h-8 p-2 rounded-full flex items-center justify-center shadow border border-gray-200 dark:border-gray-600 cursor-pointer border-solid">
|
||||
<el-icon>
|
||||
|
|
@ -88,6 +88,7 @@
|
|||
import { emitter } from '@/utils/bus.js'
|
||||
import CommandMenu from '@/components/commandMenu/index.vue'
|
||||
import { toDoc } from '@/utils/doc'
|
||||
import { isDev } from '@/utils/env.js'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const showSettingDrawer = ref(false)
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
>登 录</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item class="mb-6">
|
||||
<el-form-item v-if="isDev" class="mb-6">
|
||||
<el-button
|
||||
class="shadow shadow-active h-11 w-full"
|
||||
type="primary"
|
||||
|
|
@ -132,6 +132,7 @@
|
|||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import Logo from '@/components/logo/index.vue'
|
||||
import { isDev } from '@/utils/env.js'
|
||||
|
||||
defineOptions({
|
||||
name: 'Login'
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@
|
|||
import ExportExcel from '@/components/exportExcel/exportExcel.vue'
|
||||
import ExportTemplate from '@/components/exportExcel/exportTemplate.vue'
|
||||
import ImportExcel from '@/components/exportExcel/importExcel.vue'
|
||||
import { butler } from '@/api/autoCode'
|
||||
import { llmAuto } from '@/api/autoCode'
|
||||
import { useAppStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
|
|
@ -799,7 +799,7 @@
|
|||
const routerPaths = syncApiData.value.newApis
|
||||
.filter((item) => !item.apiGroup || !item.description)
|
||||
.map((item) => item.path)
|
||||
const res = await butler({ data: routerPaths, command: 'apiCompletion' })
|
||||
const res = await llmAuto({ data: String(routerPaths), mode: 'apiCompletion' })
|
||||
apiCompletionLoading.value = false
|
||||
if (res.code === 0) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@
|
|||
exportSysDictionary,
|
||||
importSysDictionary
|
||||
} from '@/api/sysDictionary' // 此处请自行替换地址
|
||||
import { butler, eye } from '@/api/autoCode'
|
||||
import { llmAuto } from '@/api/autoCode'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
|
@ -442,9 +442,9 @@
|
|||
const reader = new FileReader();
|
||||
reader.onload =async (e) => {
|
||||
const base64String = e.target.result;
|
||||
const res = await eye({ picture: base64String,command: 'dictEye' })
|
||||
const res = await llmAuto({ _file_path: base64String, mode:"dictEye" })
|
||||
if (res.code === 0) {
|
||||
aiPrompt.value = res.data
|
||||
aiPrompt.value = res.data.text
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
|
@ -464,9 +464,9 @@
|
|||
reader.onload = async (e) => {
|
||||
const base64String = e.target.result;
|
||||
|
||||
const res = await eye({ picture: base64String,command: 'dictEye' })
|
||||
const res = await llmAuto({ _file_path: base64String, mode:"dictEye" })
|
||||
if (res.code === 0) {
|
||||
aiPrompt.value = res.data
|
||||
aiPrompt.value = res.data.text
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
|
@ -788,9 +788,9 @@
|
|||
}
|
||||
try {
|
||||
aiGenerating.value = true
|
||||
const aiRes = await butler({
|
||||
const aiRes = await llmAuto({
|
||||
prompt: aiPrompt.value,
|
||||
command: 'dict'
|
||||
mode: 'dict'
|
||||
})
|
||||
if (aiRes && aiRes.code === 0) {
|
||||
ElMessage.success('AI 生成成功')
|
||||
|
|
@ -808,8 +808,6 @@
|
|||
} catch (e) {
|
||||
ElMessage.error('处理 AI 返回结果失败: ' + (e.message || e))
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(aiRes.msg || 'AI 生成失败')
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error('AI 调用失败: ' + (err.message || err))
|
||||
|
|
|
|||
|
|
@ -332,9 +332,6 @@
|
|||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
await getTreeData() // 重新加载数据
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="gva-search-box">
|
||||
<el-form :inline="true" :model="searchInfo">
|
||||
<el-form-item label="用户ID">
|
||||
<el-input v-model.number="searchInfo.userId" placeholder="搜索用户ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchInfo.status" placeholder="请选择" clearable>
|
||||
<el-option label="有效" :value="true" />
|
||||
<el-option label="无效" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDrawer">签发</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
row-key="ID"
|
||||
>
|
||||
<el-table-column align="left" label="ID" prop="ID" width="80" />
|
||||
<el-table-column align="left" label="用户" min-width="150">
|
||||
<template #default="scope">
|
||||
{{ scope.row.user.nickName }} ({{ scope.row.user.userName }})
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="角色ID" prop="authorityId" width="100" />
|
||||
<el-table-column align="left" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status ? 'success' : 'danger'">
|
||||
{{ scope.row.status ? '有效' : '已作废' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="过期时间" width="180">
|
||||
<template #default="scope">{{ formatDate(scope.row.expiresAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="备注" prop="remark" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column align="left" label="操作" width="220">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link icon="link" @click="openCurl(scope.row)">Curl示例</el-button>
|
||||
<el-popover v-if="scope.row.status" v-model:visible="scope.row.visible" placement="top" width="160">
|
||||
<p>确定要作废吗?</p>
|
||||
<div style="text-align: right; margin: 0">
|
||||
<el-button size="small" type="primary" link @click="scope.row.visible = false">取消</el-button>
|
||||
<el-button size="small" type="primary" @click="invalidateToken(scope.row)">确定</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button icon="delete" type="danger" link @click="scope.row.visible = true">作废</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="drawerVisible" size="400px" title="签发 API Token">
|
||||
<el-form ref="formRef" :model="form" label-width="80px">
|
||||
<el-form-item label="用户" required>
|
||||
<el-select
|
||||
v-model="form.userId"
|
||||
placeholder="请选择用户"
|
||||
filterable
|
||||
style="width:100%"
|
||||
@change="handleUserChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="item.ID"
|
||||
:label="`${item.nickName} (${item.userName})`"
|
||||
:value="item.ID"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" required>
|
||||
<el-select v-model="form.authorityId" placeholder="请选择角色" style="width:100%" :disabled="!form.userId">
|
||||
<el-option
|
||||
v-for="item in authorityOptions"
|
||||
:key="item.authorityId"
|
||||
:label="`${item.authorityName} (${item.authorityId})`"
|
||||
:value="item.authorityId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="有效期">
|
||||
<el-select v-model="form.days" placeholder="请选择" style="width:100%">
|
||||
<el-option label="1天" :value="1" />
|
||||
<el-option label="7天" :value="7" />
|
||||
<el-option label="30天" :value="30" />
|
||||
<el-option label="90天" :value="90" />
|
||||
<el-option label="永久" :value="-1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div style="flex: auto">
|
||||
<el-button @click="drawerVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitIssuer">签发JWT</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="tokenDialogVisible" title="签发成功" width="500px">
|
||||
<div style="text-align: center; margin-bottom: 20px;">
|
||||
<el-alert title="请立即复制保存,关闭后将无法再次查看完整Token" type="warning" :closable="false" show-icon />
|
||||
</div>
|
||||
<el-input type="textarea" :rows="6" v-model="tokenResult" readonly />
|
||||
<template #footer>
|
||||
<el-button @click="copyText(tokenResult)">复制</el-button>
|
||||
<el-button type="primary" @click="tokenDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="curlDrawerVisible" size="500px" title="Curl 示例">
|
||||
<div style="padding: 10px;">
|
||||
<p style="margin-bottom: 10px;">Header 方式:</p>
|
||||
<el-input type="textarea" :rows="4" v-model="curlHeader" readonly />
|
||||
<el-button style="margin-top: 5px;" size="small" @click="copyText(curlHeader)">复制</el-button>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<p style="margin-bottom: 10px;">Cookie 方式:</p>
|
||||
<el-input type="textarea" :rows="4" v-model="curlCookie" readonly />
|
||||
<el-button style="margin-top: 5px;" size="small" @click="copyText(curlCookie)">复制</el-button>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getApiTokenList,
|
||||
createApiToken,
|
||||
deleteApiToken
|
||||
} from '@/api/sysApiToken'
|
||||
import { getUserList } from '@/api/user'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatDate } from '@/utils/format'
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
const tokenDialogVisible = ref(false)
|
||||
const tokenResult = ref('')
|
||||
const curlDrawerVisible = ref(false)
|
||||
const curlHeader = ref('')
|
||||
const curlCookie = ref('')
|
||||
|
||||
const form = ref({
|
||||
userId: '',
|
||||
authorityId: '',
|
||||
days: 30,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const userOptions = ref([])
|
||||
const authorityOptions = ref([])
|
||||
|
||||
const getTableData = async () => {
|
||||
const table = await getApiTokenList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
const openDrawer = async () => {
|
||||
form.value = { userId: '', authorityId: '', days: 30, remark: '' }
|
||||
authorityOptions.value = []
|
||||
drawerVisible.value = true
|
||||
if (userOptions.value.length === 0) {
|
||||
const res = await getUserList({ page: 1, pageSize: 999 })
|
||||
if (res.code === 0) {
|
||||
userOptions.value = res.data.list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserChange = (val) => {
|
||||
form.value.authorityId = ''
|
||||
const user = userOptions.value.find(u => u.ID === val)
|
||||
if (user) {
|
||||
authorityOptions.value = user.authorities || []
|
||||
// 默认选中第一个
|
||||
if (authorityOptions.value.length > 0) {
|
||||
form.value.authorityId = authorityOptions.value[0].authorityId
|
||||
}
|
||||
} else {
|
||||
authorityOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const submitIssuer = async () => {
|
||||
if (!form.value.userId || !form.value.authorityId) {
|
||||
ElMessage.warning("请选择用户和角色")
|
||||
return
|
||||
}
|
||||
const res = await createApiToken(form.value)
|
||||
if (res.code === 0) {
|
||||
tokenResult.value = res.data.token
|
||||
drawerVisible.value = false
|
||||
tokenDialogVisible.value = true
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const invalidateToken = async (row) => {
|
||||
row.visible = false
|
||||
const res = await deleteApiToken({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("作废成功")
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const openCurl = (row) => {
|
||||
// 假设 API Host 为当前 origin
|
||||
const origin = window.location.origin
|
||||
// 构造示例 URL
|
||||
const url = `${origin}/api/menu/getMenu`
|
||||
|
||||
curlHeader.value = `curl -X POST "${url}" \
|
||||
-H "x-token: ${row.token}" \
|
||||
-H "Content-Type: application/json"`
|
||||
|
||||
curlCookie.value = `curl -X POST "${url}" \
|
||||
-b "x-token=${row.token}" \
|
||||
-H "Content-Type: application/json"`
|
||||
|
||||
curlDrawerVisible.value = true
|
||||
}
|
||||
|
||||
const copyText = (text) => {
|
||||
if (!text) return
|
||||
const input = document.createElement('textarea')
|
||||
input.value = text
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
ElMessage.success('复制成功')
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
pageSize.value = 10
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
getTableData()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<warning-bar
|
||||
href="https://www.gin-vue-admin.com/empower/"
|
||||
href="https://plugin.gin-vue-admin.com/license"
|
||||
title="此功能只针对授权用户开放,点我【购买授权】"
|
||||
/>
|
||||
<div class="gva-search-box">
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
<div>
|
||||
此功能仅针对授权用户开放,前往<a
|
||||
class="text-blue-600"
|
||||
href="https://www.gin-vue-admin.com/empower/"
|
||||
href="https://plugin.gin-vue-admin.com/license"
|
||||
target="_blank"
|
||||
>购买授权</a
|
||||
>
|
||||
|
|
@ -184,7 +184,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createWeb } from '@/api/autoCode'
|
||||
import { llmAuto } from '@/api/autoCode'
|
||||
import { ref, reactive, markRaw } from 'vue'
|
||||
import * as Vue from "vue";
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
|
|
@ -412,13 +412,13 @@ const llmAutoFunc = async () => {
|
|||
fullPrompt += `\n详细描述: ${prompt.value}`
|
||||
}
|
||||
|
||||
const res = await createWeb({web: fullPrompt, command: 'createWeb'})
|
||||
const res = await llmAuto({web: fullPrompt, mode: 'createWeb'})
|
||||
if (res.code === 0) {
|
||||
outPut.value = true
|
||||
// 添加返回的Vue组件代码到数组
|
||||
htmlFromLLM.value = res.data
|
||||
htmlFromLLM.value = res.data.text
|
||||
// 加载新生成的组件
|
||||
await loadVueComponent(res.data)
|
||||
await loadVueComponent(res.data.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@
|
|||
rollback,
|
||||
delSysHistory,
|
||||
addFunc,
|
||||
butler
|
||||
llmAuto
|
||||
} from '@/api/autoCode.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
|
@ -574,11 +574,11 @@
|
|||
return
|
||||
}
|
||||
|
||||
const aiRes = await butler({
|
||||
const aiRes = await llmAuto({
|
||||
structInfo: activeInfo.value,
|
||||
template: JSON.stringify(res.data),
|
||||
prompt: autoFunc.value.prompt,
|
||||
command: 'addFunc'
|
||||
mode: 'addFunc'
|
||||
})
|
||||
aiLoading.value = false
|
||||
if (aiRes.code === 0) {
|
||||
|
|
@ -600,9 +600,9 @@
|
|||
|
||||
const autoComplete = async () => {
|
||||
aiLoading.value = true
|
||||
const aiRes = await butler({
|
||||
const aiRes = await llmAuto({
|
||||
prompt: autoFunc.value.funcDesc,
|
||||
command: 'autoCompleteFunc'
|
||||
mode: 'autoCompleteFunc'
|
||||
})
|
||||
aiLoading.value = false
|
||||
if (aiRes.code === 0) {
|
||||
|
|
|
|||
|
|
@ -520,7 +520,7 @@
|
|||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { getDB, getTable, getColumn, butler } from '@/api/autoCode'
|
||||
import { getDB, getTable, getColumn, llmAuto } from '@/api/autoCode'
|
||||
import { getCode } from './code'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
|
||||
|
|
@ -743,11 +743,10 @@ JOINS模式下不支持导入
|
|||
}
|
||||
aiLoading.value = true
|
||||
const tableMap = await getTablesCloumn()
|
||||
const aiRes = await butler({
|
||||
const aiRes = await llmAuto({
|
||||
prompt: prompt.value,
|
||||
businessDB: formData.value.dbName || '',
|
||||
tableMap: tableMap,
|
||||
command: 'autoExportTemplate'
|
||||
tableMap: JSON.stringify(tableMap),
|
||||
mode: 'autoExportTemplate'
|
||||
})
|
||||
aiLoading.value = false
|
||||
if (aiRes.code === 0) {
|
||||
|
|
@ -800,9 +799,9 @@ JOINS模式下不支持导入
|
|||
})
|
||||
if (res.code === 0) {
|
||||
if (aiFLag) {
|
||||
const aiRes = await butler({
|
||||
data: res.data.columns,
|
||||
command: 'exportCompletion'
|
||||
const aiRes = await llmAuto({
|
||||
data: JSON.stringify(res.data.columns),
|
||||
mode: 'exportCompletion'
|
||||
})
|
||||
if (aiRes.code === 0) {
|
||||
const aiData = JSON.parse(aiRes.data)
|
||||
|
|
|
|||
|
|
@ -15,17 +15,100 @@
|
|||
<div class="el-upload__tip">请把安装包的zip拖拽至此处上传</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<!-- Plugin List Table -->
|
||||
<div style="margin-top: 20px;">
|
||||
<el-table :data="pluginList" style="width: 100%">
|
||||
<el-table-column type="expand">
|
||||
<template #default="props">
|
||||
<div style="padding: 20px;">
|
||||
<h3>API 列表</h3>
|
||||
<el-table :data="props.row.apis" border>
|
||||
<el-table-column prop="path" label="路径" />
|
||||
<el-table-column prop="method" label="方法" />
|
||||
<el-table-column prop="description" label="描述" />
|
||||
<el-table-column prop="apiGroup" label="APIGROUP" />
|
||||
</el-table>
|
||||
<h3>菜单列表</h3>
|
||||
<el-table :data="props.row.menus" row-key="name" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" border>
|
||||
<el-table-column prop="meta.title" label="标题" />
|
||||
<el-table-column prop="name" label="Name" />
|
||||
<el-table-column prop="path" label="Path" />
|
||||
</el-table>
|
||||
<h3>字典列表</h3>
|
||||
<el-table :data="props.row.dictionaries" border>
|
||||
<el-table-column prop="name" label="字典名" />
|
||||
<el-table-column prop="type" label="字典类型" />
|
||||
<el-table-column prop="desc" label="描述" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="pluginName" label="插件名称" />
|
||||
<el-table-column prop="pluginType" label="插件类型">
|
||||
<template #default="scope">
|
||||
{{ typeMap[scope.row.pluginType] || '未知类型' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link icon="delete" @click="deletePlugin(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { useUserStore } from "@/pinia";
|
||||
import { getPluginList, removePlugin } from '@/api/autoCode'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const token = userStore.token
|
||||
const pluginList = ref([])
|
||||
|
||||
const getTableData = async () => {
|
||||
const res = await getPluginList()
|
||||
if (res.code === 0) {
|
||||
pluginList.value = res.data
|
||||
}
|
||||
}
|
||||
|
||||
const typeMap = {
|
||||
"server": "后端插件",
|
||||
"web": "前端插件",
|
||||
"full": "全栈插件"
|
||||
}
|
||||
|
||||
const deletePlugin = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
'此操作将永久删除该插件及其关联的API、菜单和字典数据, 是否继续?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
const res = await removePlugin({ pluginName: row.pluginName, pluginType: row.pluginType })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
const handleSuccess = (res) => {
|
||||
if (res.code === 0) {
|
||||
|
|
@ -35,6 +118,7 @@
|
|||
msg += `${index + 1}.${item.msg}\n`
|
||||
})
|
||||
alert(msg)
|
||||
getTableData() // Refresh list on success
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="gva-search-box">
|
||||
<el-form :inline="true" :model="searchInfo">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchInfo.username" placeholder="搜索用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchInfo.status" placeholder="请选择" clearable>
|
||||
<el-option label="成功" :value="true" />
|
||||
<el-option label="失败" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button
|
||||
icon="delete"
|
||||
style="margin-left: 10px;"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onDelete"
|
||||
>删除</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column align="left" label="ID" prop="ID" width="80" />
|
||||
<el-table-column align="left" label="用户名" prop="username" width="150" />
|
||||
<el-table-column align="left" label="登录IP" prop="ip" width="150" />
|
||||
<el-table-column align="left" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status ? 'success' : 'danger'">
|
||||
{{ scope.row.status ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="详情" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.status ? '登录成功' : scope.row.errorMessage }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="浏览器/设备" prop="agent" show-overflow-tooltip />
|
||||
<el-table-column align="left" label="登录时间" width="180">
|
||||
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-popover v-model:visible="scope.row.visible" placement="top" width="160">
|
||||
<p>确定要删除吗?</p>
|
||||
<div style="text-align: right; margin: 0">
|
||||
<el-button size="small" type="primary" link @click="scope.row.visible = false">取消</el-button>
|
||||
<el-button size="small" type="primary" @click="deleteRow(scope.row)">确定</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button icon="delete" type="primary" link @click="scope.row.visible = true">删除</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getLoginLogList,
|
||||
deleteLoginLog,
|
||||
deleteLoginLogByIds
|
||||
} from '@/api/sysLoginLog'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatDate } from '@/utils/format'
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
const multipleSelection = ref([])
|
||||
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
const getTableData = async () => {
|
||||
const table = await getLoginLogList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
const deleteRow = async (row) => {
|
||||
row.visible = false
|
||||
const res = await deleteLoginLog(row)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const onDelete = async() => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async() => {
|
||||
const ids = multipleSelection.value.map(item => item.ID)
|
||||
const res = await deleteLoginLogByIds({ ids })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === ids.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
pageSize.value = 10
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 首次加载
|
||||
getTableData()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
@ -0,0 +1,793 @@
|
|||
<template>
|
||||
<div class="h-full">
|
||||
<warning-bar
|
||||
href="https://plugin.gin-vue-admin.com/license"
|
||||
title="此功能仅在开发阶段使用,用户构建本项目内的skills技能库。"
|
||||
/>
|
||||
<el-row :gutter="12" class="h-full">
|
||||
<el-col :xs="24" :sm="8" :md="6" :lg="5" class="flex flex-col gap-4 h-full">
|
||||
<el-card shadow="never" class="!border-none shrink-0">
|
||||
<div class="font-bold mb-2">AI 工具</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="tool in tools"
|
||||
:key="tool.key"
|
||||
class="px-3 py-1.5 rounded-md text-sm cursor-pointer transition-all border select-none"
|
||||
:class="activeTool === tool.key
|
||||
? 'bg-[var(--el-color-primary)] text-white border-[var(--el-color-primary)] shadow-sm'
|
||||
: 'bg-white hover:bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:bg-gray-700'"
|
||||
@click="handleToolSelect(tool.key)"
|
||||
>
|
||||
{{ tool.label }}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="!border-none shrink-0">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-bold">全局约束</span>
|
||||
<el-button type="primary" link icon="Edit" @click="openGlobalConstraint">编辑</el-button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">路径: {{ globalConstraintPath }}</div>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="!border-none flex-1 mt-2 flex flex-col min-h-0">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-bold">Skills</span>
|
||||
<el-button type="primary" link icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="skillFilter"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="搜索技能"
|
||||
class="mb-2"
|
||||
prefix-icon="Search"
|
||||
/>
|
||||
<el-scrollbar class="h-[calc(100vh-380px)]">
|
||||
<el-menu :default-active="activeSkill" class="!border-none" @select="handleSkillSelect">
|
||||
<el-menu-item
|
||||
v-for="skill in filteredSkills"
|
||||
:key="skill"
|
||||
:index="skill"
|
||||
class="!h-10 !leading-10 !my-1 !mx-1 !rounded-[4px]"
|
||||
>
|
||||
<el-icon><Document /></el-icon>
|
||||
<span class="truncate" :title="skill">{{ skill }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="16" :md="18" :lg="19" class="h-full">
|
||||
<el-card shadow="never" class="!border-none h-full flex flex-col">
|
||||
<template v-if="!activeSkill">
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<el-empty description="请选择或新建一个技能" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex justify-between items-center mb-4 pb-4 border-b border-gray-100 dark:border-gray-800">
|
||||
<div class="text-lg font-bold flex items-center gap-2">
|
||||
<span>{{ activeSkill }}</span>
|
||||
<el-tag size="small" type="info">Skill</el-tag>
|
||||
</div>
|
||||
<el-button type="primary" icon="Check" @click="saveCurrentSkill">保存配置</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="h-full">
|
||||
<el-tab-pane label="技能配置" name="config">
|
||||
<el-form :model="form" label-width="160px" class="mt-4">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
Name
|
||||
<el-tooltip content="技能的名称,例如: pr-summary" placement="top">
|
||||
<el-icon class="ml-1 cursor-pointer"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="form.name" placeholder="例如: pr-summary" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
Description
|
||||
<el-tooltip content="技能的简要描述,例如: Summarize changes in a pull request" placement="top">
|
||||
<el-icon class="ml-1 cursor-pointer"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
placeholder="例如: Summarize changes in a pull request"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
Allowed Tools
|
||||
<el-tooltip content="该技能允许使用的工具,例如: Bash(gh *)" placement="top">
|
||||
<el-icon class="ml-1 cursor-pointer"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="form.allowedTools" placeholder="可选,例如: Bash(gh *)" />
|
||||
<div class="text-xs text-gray-400 mt-1">可选字段,留空后保存会移除</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
Context
|
||||
<el-tooltip content="技能执行的上下文,例如: fork" placement="top">
|
||||
<el-icon class="ml-1 cursor-pointer"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="form.context" placeholder="可选,例如: fork" />
|
||||
<div class="text-xs text-gray-400 mt-1">可选字段,留空后保存会移除</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
Agent
|
||||
<el-tooltip content="指定执行该技能的 Agent,例如: Explore" placement="top">
|
||||
<el-icon class="ml-1 cursor-pointer"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="form.agent" placeholder="可选,例如: Explore" />
|
||||
<div class="text-xs text-gray-400 mt-1">可选字段,留空后保存会移除</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
Markdown 内容
|
||||
<el-tooltip content="SKILL.md 的具体内容,定义技能的详细逻辑" placement="top">
|
||||
<el-icon class="ml-1 cursor-pointer"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mb-2 flex flex-wrap gap-2">
|
||||
<el-button
|
||||
v-for="block in quickBlocks"
|
||||
:key="block.label"
|
||||
size="small"
|
||||
@click="appendMarkdown(block.content)"
|
||||
>
|
||||
{{ block.label }}
|
||||
</el-button>
|
||||
<el-button size="small" @click="insertFullTemplate">插入完整模板</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="form.markdown"
|
||||
type="textarea"
|
||||
:rows="20"
|
||||
:placeholder="markdownPlaceholder"
|
||||
/>
|
||||
<div class="text-xs text-gray-400 mt-1">这里是 SKILL.md 的正文内容,可自由编辑。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="脚本" name="scripts" class="mt-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="text-sm text-gray-500 bg-gray-50 dark:bg-gray-800 px-3 py-1 rounded">路径: scripts/</div>
|
||||
<el-button type="primary" icon="Plus" size="small" @click="openScriptDialog">创建脚本</el-button>
|
||||
</div>
|
||||
<el-table :data="scriptRows" style="width: 100%">
|
||||
<el-table-column prop="name" label="文件名">
|
||||
<template #default="scope">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ scope.row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link icon="Edit" @click="openScriptEditor(scope.row.name)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="scriptRows.length === 0" description="暂无脚本" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="资源" name="resources">
|
||||
<div class="flex justify-between items-center mb-4 mt-4">
|
||||
<div class="text-sm text-gray-500 bg-gray-50 dark:bg-gray-800 px-3 py-1 rounded">路径: resources/</div>
|
||||
<el-button type="primary" icon="Plus" size="small" @click="openResourceDialog">创建资源</el-button>
|
||||
</div>
|
||||
<el-table :data="resourceRows" style="width: 100%">
|
||||
<el-table-column prop="name" label="文件名">
|
||||
<template #default="scope">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ scope.row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link icon="Edit" @click="openResourceEditor(scope.row.name)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="resourceRows.length === 0" description="暂无资源" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog v-model="createDialogVisible" title="新增 Skill" width="420px">
|
||||
<el-form :model="newSkill" label-width="100px">
|
||||
<el-form-item label="Skill 名称">
|
||||
<el-input v-model="newSkill.name" placeholder="例如: pr-summary" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="newSkill.description" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="createSkill">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="scriptDialogVisible" title="创建脚本" width="420px">
|
||||
<el-form :model="newScript" label-width="100px">
|
||||
<el-form-item label="脚本类型">
|
||||
<el-select v-model="newScript.type" placeholder="选择类型">
|
||||
<el-option label="Python (.py)" value="py" />
|
||||
<el-option label="JavaScript (.js)" value="js" />
|
||||
<el-option label="Shell (.sh)" value="sh" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件名">
|
||||
<el-input v-model="newScript.name" placeholder="例如: run" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="scriptDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="createScript">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="resourceDialogVisible" title="创建资源" width="420px">
|
||||
<el-form :model="newResource" label-width="100px">
|
||||
<el-form-item label="文件名">
|
||||
<el-input v-model="newResource.name" placeholder="例如: usage" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="resourceDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="createResource">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="editorVisible" size="70%" destroy-on-close :with-header="false">
|
||||
<div class="h-full flex flex-col p-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="text-lg font-bold flex items-center gap-2">
|
||||
<el-icon><Edit /></el-icon>
|
||||
{{ editorTitle }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button @click="editorVisible = false">取消</el-button>
|
||||
<el-button type="primary" icon="Check" @click="saveEditor">保存内容</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden border border-gray-200 dark:border-gray-700 rounded-md shadow-inner">
|
||||
<v-ace-editor
|
||||
v-model:value="editorContent"
|
||||
:lang="editorLang"
|
||||
theme="github_dark"
|
||||
class="w-full h-full"
|
||||
:options="{ showPrintMargin: false, fontSize: 14 }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { QuestionFilled, Document, Plus, Search, Check, Edit } from '@element-plus/icons-vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import {
|
||||
getSkillTools,
|
||||
getSkillList,
|
||||
getSkillDetail,
|
||||
saveSkill,
|
||||
createSkillScript,
|
||||
getSkillScript,
|
||||
saveSkillScript,
|
||||
createSkillResource,
|
||||
getSkillResource,
|
||||
saveSkillResource,
|
||||
getGlobalConstraint,
|
||||
saveGlobalConstraint
|
||||
} from '@/api/skills'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import 'ace-builds/src-noconflict/mode-javascript'
|
||||
import 'ace-builds/src-noconflict/mode-python'
|
||||
import 'ace-builds/src-noconflict/mode-sh'
|
||||
import 'ace-builds/src-noconflict/mode-markdown'
|
||||
import 'ace-builds/src-noconflict/theme-github_dark'
|
||||
|
||||
defineOptions({
|
||||
name: 'Skills'
|
||||
})
|
||||
|
||||
const tools = ref([
|
||||
{ key: 'copilot', label: 'Copilot' },
|
||||
{ key: 'claude', label: 'Claude' },
|
||||
{ key: 'cursor', label: 'Cursor' },
|
||||
{ key: 'trae', label: 'Trae' },
|
||||
{ key: 'codex', label: 'Codex' }
|
||||
])
|
||||
const activeTool = ref('claude')
|
||||
const skills = ref([])
|
||||
const activeSkill = ref('')
|
||||
const skillFilter = ref('')
|
||||
const activeTab = ref('config')
|
||||
const globalConstraintExists = ref(false)
|
||||
|
||||
const toolDirMap = {
|
||||
copilot: '.aone_copilot',
|
||||
claude: '.claude',
|
||||
cursor: '.cursor',
|
||||
trae: '.trae',
|
||||
codex: '.codex'
|
||||
}
|
||||
|
||||
const globalConstraintPath = computed(() => {
|
||||
if (!activeTool.value) return 'skills/README.md'
|
||||
const toolDir = toolDirMap[activeTool.value] || `.${activeTool.value}`
|
||||
return `${toolDir}/skills/README.md`
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
allowedTools: '',
|
||||
context: '',
|
||||
agent: '',
|
||||
markdown: ''
|
||||
})
|
||||
|
||||
const markdownPlaceholder =
|
||||
'建议包含:技能用途、输入、输出、步骤与示例。\n\n示例:\n## 技能用途\n请描述技能的目标与限制。\n\n## 输入\n- 输入1...\n\n## 输出\n- 输出1...\n'
|
||||
|
||||
const quickBlocks = [
|
||||
{ label: '用途', content: '\n## 技能用途\n请描述技能目标与适用场景。\n' },
|
||||
{ label: '输入', content: '\n## 输入\n- 输入字段与格式说明。\n' },
|
||||
{ label: '输出', content: '\n## 输出\n- 输出字段与格式说明。\n' },
|
||||
{ label: '步骤', content: '\n## 关键步骤\n1. 第一步\n2. 第二步\n' },
|
||||
{ label: '示例', content: '\n## 示例\n在此补充示例。\n' },
|
||||
{ label: '注意事项', content: '\n## 注意事项\n- 需要注意的限制或风险。\n' }
|
||||
]
|
||||
|
||||
const scripts = ref([])
|
||||
const resources = ref([])
|
||||
|
||||
const scriptRows = computed(() => skillsFilesToRows(scripts.value))
|
||||
const resourceRows = computed(() => skillsFilesToRows(resources.value))
|
||||
|
||||
const createDialogVisible = ref(false)
|
||||
const scriptDialogVisible = ref(false)
|
||||
const resourceDialogVisible = ref(false)
|
||||
|
||||
const newSkill = reactive({
|
||||
name: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const newScript = reactive({
|
||||
name: '',
|
||||
type: 'py'
|
||||
})
|
||||
|
||||
const newResource = reactive({
|
||||
name: ''
|
||||
})
|
||||
|
||||
const editorVisible = ref(false)
|
||||
const editorContent = ref('')
|
||||
const editorFileName = ref('')
|
||||
const editorType = ref('script')
|
||||
const editorLang = ref('text')
|
||||
|
||||
const editorTitle = computed(() => {
|
||||
if (!editorFileName.value) {
|
||||
return editorType.value === 'constraint' ? '全局约束' : '文件编辑'
|
||||
}
|
||||
if (editorType.value === 'script') return `脚本:${editorFileName.value}`
|
||||
if (editorType.value === 'resource') return `资源:${editorFileName.value}`
|
||||
if (editorType.value === 'constraint') return `全局约束:${editorFileName.value}`
|
||||
return `文件编辑:${editorFileName.value}`
|
||||
})
|
||||
|
||||
const filteredSkills = computed(() => {
|
||||
if (!skillFilter.value) return skills.value
|
||||
return skills.value.filter((item) => item.toLowerCase().includes(skillFilter.value.toLowerCase()))
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTools()
|
||||
await loadSkills()
|
||||
})
|
||||
|
||||
async function loadTools() {
|
||||
try {
|
||||
const res = await getSkillTools()
|
||||
if (res.code === 0 && res.data?.tools?.length) {
|
||||
tools.value = res.data.tools
|
||||
if (!tools.value.find((item) => item.key === activeTool.value)) {
|
||||
activeTool.value = tools.value[0]?.key || 'claude'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.warning('获取工具列表失败,使用默认列表')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkills() {
|
||||
if (!activeTool.value) return
|
||||
try {
|
||||
const res = await getSkillList({ tool: activeTool.value })
|
||||
if (res.code === 0) {
|
||||
skills.value = res.data?.skills || []
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('获取技能列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkillDetail(skillName) {
|
||||
if (!activeTool.value || !skillName) return
|
||||
try {
|
||||
const res = await getSkillDetail({ tool: activeTool.value, skill: skillName })
|
||||
if (res.code === 0) {
|
||||
const detail = res.data?.detail
|
||||
activeSkill.value = detail?.skill || skillName
|
||||
form.name = detail?.meta?.name || skillName
|
||||
form.description = detail?.meta?.description || ''
|
||||
form.allowedTools = detail?.meta?.allowedTools || ''
|
||||
form.context = detail?.meta?.context || ''
|
||||
form.agent = detail?.meta?.agent || ''
|
||||
form.markdown = detail?.markdown || ''
|
||||
scripts.value = detail?.scripts || []
|
||||
resources.value = detail?.resources || []
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('获取技能详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function openGlobalConstraint() {
|
||||
if (!activeTool.value) {
|
||||
ElMessage.warning('请先选择工具')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getGlobalConstraint({ tool: activeTool.value })
|
||||
if (res.code === 0) {
|
||||
globalConstraintExists.value = !!res.data?.exists
|
||||
if (!globalConstraintExists.value) {
|
||||
ElMessage.info('未检测到 README.md,保存后将创建该文件')
|
||||
}
|
||||
openEditor('constraint', 'README.md', res.data?.content || '')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('读取全局约束失败')
|
||||
}
|
||||
}
|
||||
|
||||
function resetDetail() {
|
||||
activeSkill.value = ''
|
||||
form.name = ''
|
||||
form.description = ''
|
||||
form.allowedTools = ''
|
||||
form.context = ''
|
||||
form.agent = ''
|
||||
form.markdown = ''
|
||||
scripts.value = []
|
||||
resources.value = []
|
||||
activeTab.value = 'config'
|
||||
}
|
||||
|
||||
function handleToolSelect(key) {
|
||||
activeTool.value = key
|
||||
resetDetail()
|
||||
globalConstraintExists.value = false
|
||||
loadSkills()
|
||||
}
|
||||
|
||||
function handleSkillSelect(skillName) {
|
||||
loadSkillDetail(skillName)
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
newSkill.name = ''
|
||||
newSkill.description = ''
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function createSkill() {
|
||||
if (!newSkill.name.trim()) {
|
||||
ElMessage.warning('请输入 Skill 名称')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
tool: activeTool.value,
|
||||
skill: newSkill.name.trim(),
|
||||
meta: {
|
||||
name: newSkill.name.trim(),
|
||||
description: newSkill.description.trim() || '请补充技能描述',
|
||||
allowedTools: 'Bash(gh *)',
|
||||
context: 'fork',
|
||||
agent: 'Explore'
|
||||
},
|
||||
markdown: defaultSkillTemplate()
|
||||
}
|
||||
try {
|
||||
const res = await saveSkill(payload)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('创建成功')
|
||||
createDialogVisible.value = false
|
||||
await loadSkills()
|
||||
await loadSkillDetail(payload.skill)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCurrentSkill() {
|
||||
if (!activeSkill.value) return
|
||||
if (!form.name.trim()) {
|
||||
ElMessage.warning('Name 不能为空')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
tool: activeTool.value,
|
||||
skill: activeSkill.value,
|
||||
meta: {
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim(),
|
||||
allowedTools: form.allowedTools.trim(),
|
||||
context: form.context.trim(),
|
||||
agent: form.agent.trim()
|
||||
},
|
||||
markdown: form.markdown
|
||||
}
|
||||
|
||||
let syncTools = []
|
||||
try {
|
||||
await ElMessageBox.confirm('是否同步到其他 AI 客户端工具?', '同步提示', {
|
||||
confirmButtonText: '同步',
|
||||
cancelButtonText: '仅当前',
|
||||
type: 'warning'
|
||||
})
|
||||
syncTools = tools.value
|
||||
.map((item) => item.key)
|
||||
.filter((key) => key && key !== activeTool.value)
|
||||
} catch (e) {
|
||||
syncTools = []
|
||||
}
|
||||
|
||||
if (syncTools.length) {
|
||||
payload.syncTools = syncTools
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await saveSkill(payload)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
function appendMarkdown(content) {
|
||||
form.markdown = `${form.markdown || ''}${content}`
|
||||
}
|
||||
|
||||
function insertFullTemplate() {
|
||||
if (!form.markdown.trim()) {
|
||||
form.markdown = defaultSkillTemplate()
|
||||
return
|
||||
}
|
||||
form.markdown = `${form.markdown}\n${defaultSkillTemplate()}`
|
||||
}
|
||||
|
||||
function openScriptDialog() {
|
||||
if (!activeSkill.value) {
|
||||
ElMessage.warning('请先选择技能')
|
||||
return
|
||||
}
|
||||
newScript.name = ''
|
||||
newScript.type = 'py'
|
||||
scriptDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function createScript() {
|
||||
if (!newScript.name.trim()) {
|
||||
ElMessage.warning('请输入脚本文件名')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await createSkillScript({
|
||||
tool: activeTool.value,
|
||||
skill: activeSkill.value,
|
||||
fileName: newScript.name.trim(),
|
||||
scriptType: newScript.type
|
||||
})
|
||||
if (res.code === 0) {
|
||||
scriptDialogVisible.value = false
|
||||
await loadSkillDetail(activeSkill.value)
|
||||
openEditor('script', res.data.fileName, res.data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('创建脚本失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function openScriptEditor(fileName) {
|
||||
if (!fileName) return
|
||||
try {
|
||||
const res = await getSkillScript({
|
||||
tool: activeTool.value,
|
||||
skill: activeSkill.value,
|
||||
fileName
|
||||
})
|
||||
if (res.code === 0) {
|
||||
openEditor('script', fileName, res.data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('读取脚本失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openResourceDialog() {
|
||||
if (!activeSkill.value) {
|
||||
ElMessage.warning('请先选择技能')
|
||||
return
|
||||
}
|
||||
newResource.name = ''
|
||||
resourceDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function createResource() {
|
||||
if (!newResource.name.trim()) {
|
||||
ElMessage.warning('请输入资源文件名')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await createSkillResource({
|
||||
tool: activeTool.value,
|
||||
skill: activeSkill.value,
|
||||
fileName: newResource.name.trim()
|
||||
})
|
||||
if (res.code === 0) {
|
||||
resourceDialogVisible.value = false
|
||||
await loadSkillDetail(activeSkill.value)
|
||||
openEditor('resource', res.data.fileName, res.data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('创建资源失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function openResourceEditor(fileName) {
|
||||
if (!fileName) return
|
||||
try {
|
||||
const res = await getSkillResource({
|
||||
tool: activeTool.value,
|
||||
skill: activeSkill.value,
|
||||
fileName
|
||||
})
|
||||
if (res.code === 0) {
|
||||
openEditor('resource', fileName, res.data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('读取资源失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openEditor(type, fileName, content) {
|
||||
editorType.value = type
|
||||
editorFileName.value = fileName
|
||||
editorContent.value = content || ''
|
||||
editorLang.value = detectLang(fileName)
|
||||
editorVisible.value = true
|
||||
}
|
||||
|
||||
async function saveEditor() {
|
||||
if (!editorFileName.value) return
|
||||
try {
|
||||
if (editorType.value === 'script') {
|
||||
const res = await saveSkillScript({
|
||||
tool: activeTool.value,
|
||||
skill: activeSkill.value,
|
||||
fileName: editorFileName.value,
|
||||
content: editorContent.value
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
} else if (editorType.value === 'resource') {
|
||||
const res = await saveSkillResource({
|
||||
tool: activeTool.value,
|
||||
skill: activeSkill.value,
|
||||
fileName: editorFileName.value,
|
||||
content: editorContent.value
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
} else if (editorType.value === 'constraint') {
|
||||
let syncTools = []
|
||||
if (tools.value.length > 1) {
|
||||
try {
|
||||
await ElMessageBox.confirm('是否同步到其他 AI 客户端工具?', '同步提示', {
|
||||
confirmButtonText: '同步',
|
||||
cancelButtonText: '仅当前',
|
||||
type: 'warning'
|
||||
})
|
||||
syncTools = tools.value
|
||||
.map((item) => item.key)
|
||||
.filter((key) => key && key !== activeTool.value)
|
||||
} catch (e) {
|
||||
syncTools = []
|
||||
}
|
||||
}
|
||||
|
||||
const res = await saveGlobalConstraint({
|
||||
tool: activeTool.value,
|
||||
content: editorContent.value,
|
||||
syncTools
|
||||
})
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error('保存失败')
|
||||
return
|
||||
}
|
||||
globalConstraintExists.value = true
|
||||
ElMessage.success(syncTools.length ? '保存并同步成功' : '保存成功')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
function detectLang(fileName) {
|
||||
if (!fileName) return 'text'
|
||||
const lower = fileName.toLowerCase()
|
||||
if (lower.endsWith('.py')) return 'python'
|
||||
if (lower.endsWith('.js')) return 'javascript'
|
||||
if (lower.endsWith('.sh')) return 'sh'
|
||||
if (lower.endsWith('.md')) return 'markdown'
|
||||
return 'text'
|
||||
}
|
||||
|
||||
function defaultSkillTemplate() {
|
||||
return (
|
||||
'## 技能用途\n请在这里描述技能的目标、适用场景与限制条件。\n\n' +
|
||||
'## 输入\n- 请补充输入格式与示例。\n\n' +
|
||||
'## 输出\n- 请补充输出格式与示例。\n\n' +
|
||||
'## 关键步骤\n1. 第一步\n2. 第二步\n\n' +
|
||||
'## 示例\n在此补充一到两个典型示例。\n'
|
||||
)
|
||||
}
|
||||
|
||||
function skillsFilesToRows(list) {
|
||||
return (list || []).map((name) => ({ name }))
|
||||
}
|
||||
</script>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue