feat: 更新后端skill服务
- 更新 skill_handler.go handler层 - 更新 skill.go model层 - 更新 skill_repo.go repository层 - 更新 skill_service.go service层 - 更新 main.go 入口文件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -121,19 +121,15 @@ func ensureAdminWorkspace() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 admin 工作空间
|
// 创建 skills 目录结构: core/agents/skills/{system,user}
|
||||||
workspacePath := filepath.Join(projectRoot, "account", "admin")
|
skillsRoot := filepath.Join(projectRoot, "core", "agents", "skills")
|
||||||
if err := os.MkdirAll(workspacePath, 0755); err != nil {
|
for _, dir := range []string{"system", "user"} {
|
||||||
log.Printf("Warning: failed to create admin workspace: %v", err)
|
if err := os.MkdirAll(filepath.Join(skillsRoot, dir), 0755); err != nil {
|
||||||
return
|
log.Printf("Warning: failed to create skills directory %s: %v", dir, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建子目录: skills(技能), scripts(脚本), sandbox(沙盒), files(文件), temp(临时)
|
log.Printf("Skills workspace created at: %s", skillsRoot)
|
||||||
for _, dir := range []string{"skills", "scripts", "sandbox", "files", "temp"} {
|
|
||||||
os.MkdirAll(filepath.Join(workspacePath, dir), 0755)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Admin workspace created at: %s", workspacePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -240,6 +236,11 @@ func main() {
|
|||||||
|
|
||||||
// 使用GORM Migrator添加缺失的列
|
// 使用GORM Migrator添加缺失的列
|
||||||
migrator := db.Migrator()
|
migrator := db.Migrator()
|
||||||
|
|
||||||
|
// Skill 表迁移
|
||||||
|
if !migrator.HasColumn(&model.Skill{}, "created_by") {
|
||||||
|
migrator.AddColumn(&model.Skill{}, "created_by")
|
||||||
|
}
|
||||||
if !migrator.HasColumn(&model.Tool{}, "security_level") {
|
if !migrator.HasColumn(&model.Tool{}, "security_level") {
|
||||||
migrator.AddColumn(&model.Tool{}, "security_level")
|
migrator.AddColumn(&model.Tool{}, "security_level")
|
||||||
}
|
}
|
||||||
@@ -306,12 +307,12 @@ func main() {
|
|||||||
log.Println("Default tools initialized")
|
log.Println("Default tools initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4.3 初始化 skills
|
// 4.3 初始化 skills(已禁用自动加载,如需启用请调用 /skill/sync 接口)
|
||||||
if err := skillService.InitSkills(); err != nil {
|
// if err := skillService.InitSkills(); err != nil {
|
||||||
log.Printf("Warning: Failed to init skills: %v", err)
|
// log.Printf("Warning: Failed to init skills: %v", err)
|
||||||
} else {
|
// } else {
|
||||||
log.Println("Skills initialized")
|
// log.Println("Skills initialized")
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 6. 初始化 Handler
|
// 6. 初始化 Handler
|
||||||
dbHandler := handler.NewDatabaseHandler(dbService)
|
dbHandler := handler.NewDatabaseHandler(dbService)
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"x-agents/server/internal/model"
|
"x-agents/server/internal/model"
|
||||||
"x-agents/server/internal/service"
|
"x-agents/server/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SkillHandler 技能处理器
|
// SkillHandler 技能处理器
|
||||||
@@ -93,21 +96,98 @@ func (h *SkillHandler) GetByID(c *gin.Context) {
|
|||||||
|
|
||||||
// Create 创建技能
|
// Create 创建技能
|
||||||
// @Summary 创建技能
|
// @Summary 创建技能
|
||||||
// @Description 创建新的技能
|
// @Description 创建新的技能,支持文件上传。管理员用户(admin)上传为system技能,存到core/agents/skills/system/;其他用户上传为user技能,存到core/agents/skills/user/
|
||||||
// @Tags 技能管理
|
// @Tags 技能管理
|
||||||
// @Accept json
|
// @Accept multipart/form-data
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param skill body model.Skill true "技能信息"
|
// @Param skill_name formData string true "技能名称"
|
||||||
|
// @Param skill_desc formData string false "技能描述"
|
||||||
|
// @Param skill_type formData string false "技能类型(system/user),不传则根据用户角色自动判断"
|
||||||
|
// @Param file formData file false "技能文件(SKILL.md)"
|
||||||
// @Success 200 {object} map[string]interface{} "{"message": "skill created", "skill": {}}"
|
// @Success 200 {object} map[string]interface{} "{"message": "skill created", "skill": {}}"
|
||||||
// @Router /skill/add [post]
|
// @Router /skill/add [post]
|
||||||
func (h *SkillHandler) Create(c *gin.Context) {
|
func (h *SkillHandler) Create(c *gin.Context) {
|
||||||
var skill model.Skill
|
// 获取当前用户信息
|
||||||
if err := c.ShouldBindJSON(&skill); err != nil {
|
username, exists := c.Get("username")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
if !exists {
|
||||||
|
username = ""
|
||||||
|
}
|
||||||
|
userID, _ := c.Get("userID")
|
||||||
|
|
||||||
|
skillName := c.PostForm("skill_name")
|
||||||
|
skillDesc := c.PostForm("skill_desc")
|
||||||
|
skillType := c.PostForm("skill_type")
|
||||||
|
|
||||||
|
if skillName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "skill_name is required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.skillService.CreateSkill(&skill); err != nil {
|
// 确定技能类型:优先使用传入的type,否则根据用户角色判断
|
||||||
|
if skillType == "" {
|
||||||
|
// 判断是否为管理员用户
|
||||||
|
if username == "admin" {
|
||||||
|
skillType = "system"
|
||||||
|
} else {
|
||||||
|
skillType = "user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取项目根目录
|
||||||
|
projectRoot := h.getProjectRoot()
|
||||||
|
if projectRoot == "" {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot find project root"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据技能类型确定存储路径
|
||||||
|
skillDir := "user"
|
||||||
|
if skillType == "system" {
|
||||||
|
skillDir = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建技能目录
|
||||||
|
skillPath := filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skillName)
|
||||||
|
if err := os.MkdirAll(skillPath, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create skill directory: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件上传
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err == nil {
|
||||||
|
// 保存上传的文件为 SKILL.md
|
||||||
|
skillFilePath := filepath.Join(skillPath, "SKILL.md")
|
||||||
|
if err := c.SaveUploadedFile(file, skillFilePath); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save skill file: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有上传文件,创建一个默认的 SKILL.md
|
||||||
|
defaultContent := "---\nname: " + skillName + "\ndescription: " + skillDesc + "\n---\n\n# " + skillName + "\n\n" + skillDesc
|
||||||
|
skillFilePath := filepath.Join(skillPath, "SKILL.md")
|
||||||
|
if err := os.WriteFile(skillFilePath, []byte(defaultContent), 0644); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create skill file: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建技能记录
|
||||||
|
skill := &model.Skill{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
SkillName: skillName,
|
||||||
|
SkillType: skillType,
|
||||||
|
SkillDesc: skillDesc,
|
||||||
|
Path: filepath.Join(skillPath, "SKILL.md"),
|
||||||
|
Status: "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录创建者
|
||||||
|
if userIDStr, ok := userID.(string); ok {
|
||||||
|
skill.CreatedBy = userIDStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.skillService.CreateSkill(skill); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -115,6 +195,35 @@ func (h *SkillHandler) Create(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "skill created", "skill": skill})
|
c.JSON(http.StatusOK, gin.H{"message": "skill created", "skill": skill})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getProjectRoot 获取项目根目录
|
||||||
|
func (h *SkillHandler) getProjectRoot() string {
|
||||||
|
execPath, _ := os.Getwd()
|
||||||
|
projectRoot := execPath
|
||||||
|
|
||||||
|
// 如果当前目录名为 server,向上找一级
|
||||||
|
baseName := filepath.Base(execPath)
|
||||||
|
if baseName == "server" {
|
||||||
|
projectRoot = filepath.Dir(execPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试向上查找包含 .git 的目录
|
||||||
|
if _, err := os.Stat(filepath.Join(projectRoot, ".git")); os.IsNotExist(err) {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
parent := filepath.Dir(projectRoot)
|
||||||
|
if parent == projectRoot {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(parent, ".git")); err == nil {
|
||||||
|
projectRoot = parent
|
||||||
|
break
|
||||||
|
}
|
||||||
|
projectRoot = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectRoot
|
||||||
|
}
|
||||||
|
|
||||||
// Update 更新技能
|
// Update 更新技能
|
||||||
// @Summary 更新技能
|
// @Summary 更新技能
|
||||||
// @Description 更新技能信息
|
// @Description 更新技能信息
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Skill struct {
|
|||||||
SkillDesc string `json:"skill_desc" gorm:"type:text"`
|
SkillDesc string `json:"skill_desc" gorm:"type:text"`
|
||||||
Path string `json:"path" gorm:"size:500"` // skill 文件路径
|
Path string `json:"path" gorm:"size:500"` // skill 文件路径
|
||||||
Status string `json:"status" gorm:"size:20;default:'active'"`
|
Status string `json:"status" gorm:"size:20;default:'active'"`
|
||||||
|
CreatedBy string `json:"created_by" gorm:"size:100"` // 创建者用户名
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,8 +53,25 @@ func (r *SkillRepository) FindByName(skillName string) (*model.Skill, error) {
|
|||||||
return &skill, nil
|
return &skill, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update 更新技能(只更新传入的字段)
|
||||||
func (r *SkillRepository) Update(skill *model.Skill) error {
|
func (r *SkillRepository) Update(skill *model.Skill) error {
|
||||||
return r.db.Save(skill).Error
|
updates := make(map[string]interface{})
|
||||||
|
if skill.SkillName != "" {
|
||||||
|
updates["skill_name"] = skill.SkillName
|
||||||
|
}
|
||||||
|
if skill.SkillDesc != "" {
|
||||||
|
updates["skill_desc"] = skill.SkillDesc
|
||||||
|
}
|
||||||
|
if skill.SkillType != "" {
|
||||||
|
updates["skill_type"] = skill.SkillType
|
||||||
|
}
|
||||||
|
if skill.Status != "" {
|
||||||
|
updates["status"] = skill.Status
|
||||||
|
}
|
||||||
|
if skill.Path != "" {
|
||||||
|
updates["path"] = skill.Path
|
||||||
|
}
|
||||||
|
return r.db.Model(&model.Skill{}).Where("id = ?", skill.ID).Updates(updates).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SkillRepository) Delete(id string) error {
|
func (r *SkillRepository) Delete(id string) error {
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ func (s *SkillService) InitSkills() error {
|
|||||||
|
|
||||||
var totalCount int
|
var totalCount int
|
||||||
|
|
||||||
// 扫描 system skills: account/admin/skills
|
// 扫描 system skills: core/agents/skills/system
|
||||||
systemSkillsPath := filepath.Join(projectRoot, "account", "admin", "skills")
|
systemSkillsPath := filepath.Join(projectRoot, "core", "agents", "skills", "system")
|
||||||
if _, err := os.Stat(systemSkillsPath); err == nil {
|
if _, err := os.Stat(systemSkillsPath); err == nil {
|
||||||
systemSkills, err := s.scanSkillsDirectory(systemSkillsPath, "system")
|
systemSkills, err := s.scanSkillsDirectory(systemSkillsPath, "system")
|
||||||
if err == nil && len(systemSkills) > 0 {
|
if err == nil && len(systemSkills) > 0 {
|
||||||
@@ -88,22 +88,14 @@ func (s *SkillService) InitSkills() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扫描 user skills: account/{username}/skills (除了 admin)
|
// 扫描 user skills: core/agents/skills/user
|
||||||
accountPath := filepath.Join(projectRoot, "account")
|
userSkillsPath := filepath.Join(projectRoot, "core", "agents", "skills", "user")
|
||||||
entries, err := os.ReadDir(accountPath)
|
if _, err := os.Stat(userSkillsPath); err == nil {
|
||||||
if err == nil {
|
userSkills, err := s.scanSkillsDirectory(userSkillsPath, "user")
|
||||||
for _, entry := range entries {
|
if err == nil && len(userSkills) > 0 {
|
||||||
if !entry.IsDir() || entry.Name() == "admin" {
|
s.skillRepo.DeleteByType("user")
|
||||||
continue
|
s.skillRepo.UpsertBatch(userSkills)
|
||||||
}
|
totalCount += len(userSkills)
|
||||||
userSkillsPath := filepath.Join(accountPath, entry.Name(), "skills")
|
|
||||||
if _, err := os.Stat(userSkillsPath); err == nil {
|
|
||||||
userSkills, err := s.scanSkillsDirectory(userSkillsPath, "user")
|
|
||||||
if err == nil && len(userSkills) > 0 {
|
|
||||||
s.skillRepo.UpsertBatch(userSkills)
|
|
||||||
totalCount += len(userSkills)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user