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:
PiexlMax(奇淼 2026-01-30 14:33:57 +08:00 committed by GitHub
parent 755373468c
commit 876017115c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 3324 additions and 102 deletions

View File

@ -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)
未授权去除版权信息将依法追究法律责任

View File

@ -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)
}

View File

@ -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
)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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{

View File

@ -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{},

View File

@ -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()...)
}

View File

@ -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) // 文件上传下载分类

View File

@ -349,7 +349,16 @@ func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error {
return err // 其他错误
}
// 检查目录中是否包含go文件
noGoFiles, err := g.hasGoFilesRecursive(dirPath)
if err != nil {
return err
}
// hasGoFilesRecursive 返回 false 表示发现了 go 文件
if noGoFiles {
return os.RemoveAll(dirPath)
}
return nil
}
// cleanupRelatedApiAndMenus 清理相关的API和菜单记录

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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:备注"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -9,7 +9,7 @@ import (
func Menu(ctx context.Context) {
entities := []model.SysBaseMenu{
{
ParentId: 24,
ParentId: 9,
Path: "anInfo",
Name: "anInfo",
Hidden: false,

View File

@ -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

View File

@ -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]
}

View File

@ -0,0 +1,5 @@
package plugin
import (
_ "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement"
)

View File

@ -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方法中实现并添加如下方法

View File

@ -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
)

View File

@ -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
}
}

View File

@ -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)

View File

@ -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) // 获取登录日志列表
}
}

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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,9 +84,17 @@ 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 {
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)
}

View File

@ -18,9 +18,12 @@ type ServiceGroup struct {
SysExportTemplateService
SysParamsService
SysVersionService
SkillsService
AutoCodePlugin autoCodePlugin
AutoCodePackage autoCodePackage
AutoCodeHistory autoCodeHistory
AutoCodeTemplate autoCodeTemplate
SysErrorService
LoginLogService
ApiTokenService
}

View File

@ -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
}

View File

@ -107,7 +107,6 @@ func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context,
llmReq := common.JSONMap{
"mode": "solution",
"command": "solution",
"info": info,
"form": form,
}
@ -115,7 +114,7 @@ func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context,
// 调用服务层 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 {
// 即使生成失败也标记为完成,避免任务卡住

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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"},

View File

@ -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"},

View File

@ -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"}},

View File

@ -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
}

View File

@ -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"

View File

@ -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
})
}

96
web/src/api/skills.js Normal file
View File

@ -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
})
}

View File

@ -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
})
}

View File

@ -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
})
}

View File

@ -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)

3
web/src/utils/env.js Normal file
View File

@ -0,0 +1,3 @@
export const isDev = import.meta.env.DEV;
export const isProd = import.meta.env.PROD;

View File

@ -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) => {

View File

@ -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)

View File

@ -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'

View File

@ -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 {

View File

@ -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))

View File

@ -332,9 +332,6 @@
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
await getTreeData() //
}
})

View File

@ -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>

View File

@ -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)
})

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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)

View File

@ -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)
}

View File

@ -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>

View File

@ -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>

View File

@ -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'
@ -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