diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index b5b3045..e9113e9 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -121,19 +121,15 @@ func ensureAdminWorkspace() { } } - // 创建 admin 工作空间 - workspacePath := filepath.Join(projectRoot, "account", "admin") - if err := os.MkdirAll(workspacePath, 0755); err != nil { - log.Printf("Warning: failed to create admin workspace: %v", err) - return + // 创建 skills 目录结构: core/agents/skills/{system,user} + skillsRoot := filepath.Join(projectRoot, "core", "agents", "skills") + for _, dir := range []string{"system", "user"} { + if err := os.MkdirAll(filepath.Join(skillsRoot, dir), 0755); err != nil { + log.Printf("Warning: failed to create skills directory %s: %v", dir, err) + } } - // 创建子目录: skills(技能), scripts(脚本), sandbox(沙盒), files(文件), temp(临时) - for _, dir := range []string{"skills", "scripts", "sandbox", "files", "temp"} { - os.MkdirAll(filepath.Join(workspacePath, dir), 0755) - } - - log.Printf("Admin workspace created at: %s", workspacePath) + log.Printf("Skills workspace created at: %s", skillsRoot) } func main() { @@ -240,6 +236,11 @@ func main() { // 使用GORM 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") { migrator.AddColumn(&model.Tool{}, "security_level") } @@ -306,12 +307,12 @@ func main() { log.Println("Default tools initialized") } - // 4.3 初始化 skills - if err := skillService.InitSkills(); err != nil { - log.Printf("Warning: Failed to init skills: %v", err) - } else { - log.Println("Skills initialized") - } + // 4.3 初始化 skills(已禁用自动加载,如需启用请调用 /skill/sync 接口) + // if err := skillService.InitSkills(); err != nil { + // log.Printf("Warning: Failed to init skills: %v", err) + // } else { + // log.Println("Skills initialized") + // } // 6. 初始化 Handler dbHandler := handler.NewDatabaseHandler(dbService) diff --git a/server/internal/handler/skill_handler.go b/server/internal/handler/skill_handler.go index 2b717b5..d1ec785 100644 --- a/server/internal/handler/skill_handler.go +++ b/server/internal/handler/skill_handler.go @@ -2,11 +2,14 @@ package handler import ( "net/http" + "os" + "path/filepath" "x-agents/server/internal/model" "x-agents/server/internal/service" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) // SkillHandler 技能处理器 @@ -93,21 +96,98 @@ func (h *SkillHandler) GetByID(c *gin.Context) { // Create 创建技能 // @Summary 创建技能 -// @Description 创建新的技能 +// @Description 创建新的技能,支持文件上传。管理员用户(admin)上传为system技能,存到core/agents/skills/system/;其他用户上传为user技能,存到core/agents/skills/user/ // @Tags 技能管理 -// @Accept json +// @Accept multipart/form-data // @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": {}}" // @Router /skill/add [post] func (h *SkillHandler) Create(c *gin.Context) { - var skill model.Skill - if err := c.ShouldBindJSON(&skill); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + // 获取当前用户信息 + username, exists := c.Get("username") + 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 } - 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()}) return } @@ -115,6 +195,35 @@ func (h *SkillHandler) Create(c *gin.Context) { 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 更新技能 // @Summary 更新技能 // @Description 更新技能信息 diff --git a/server/internal/model/skill.go b/server/internal/model/skill.go index 0efec74..5b58a96 100644 --- a/server/internal/model/skill.go +++ b/server/internal/model/skill.go @@ -15,6 +15,7 @@ type Skill struct { SkillDesc string `json:"skill_desc" gorm:"type:text"` Path string `json:"path" gorm:"size:500"` // skill 文件路径 Status string `json:"status" gorm:"size:20;default:'active'"` + CreatedBy string `json:"created_by" gorm:"size:100"` // 创建者用户名 CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/server/internal/repository/skill_repo.go b/server/internal/repository/skill_repo.go index 2a2a31a..fafb6c9 100644 --- a/server/internal/repository/skill_repo.go +++ b/server/internal/repository/skill_repo.go @@ -53,8 +53,25 @@ func (r *SkillRepository) FindByName(skillName string) (*model.Skill, error) { return &skill, nil } +// Update 更新技能(只更新传入的字段) 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 { diff --git a/server/internal/service/skill_service.go b/server/internal/service/skill_service.go index 8d8709a..b5a2f9f 100644 --- a/server/internal/service/skill_service.go +++ b/server/internal/service/skill_service.go @@ -77,8 +77,8 @@ func (s *SkillService) InitSkills() error { var totalCount int - // 扫描 system skills: account/admin/skills - systemSkillsPath := filepath.Join(projectRoot, "account", "admin", "skills") + // 扫描 system skills: core/agents/skills/system + systemSkillsPath := filepath.Join(projectRoot, "core", "agents", "skills", "system") if _, err := os.Stat(systemSkillsPath); err == nil { systemSkills, err := s.scanSkillsDirectory(systemSkillsPath, "system") if err == nil && len(systemSkills) > 0 { @@ -88,22 +88,14 @@ func (s *SkillService) InitSkills() error { } } - // 扫描 user skills: account/{username}/skills (除了 admin) - accountPath := filepath.Join(projectRoot, "account") - entries, err := os.ReadDir(accountPath) - if err == nil { - for _, entry := range entries { - if !entry.IsDir() || entry.Name() == "admin" { - continue - } - 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) - } - } + // 扫描 user skills: core/agents/skills/user + userSkillsPath := filepath.Join(projectRoot, "core", "agents", "skills", "user") + if _, err := os.Stat(userSkillsPath); err == nil { + userSkills, err := s.scanSkillsDirectory(userSkillsPath, "user") + if err == nil && len(userSkills) > 0 { + s.skillRepo.DeleteByType("user") + s.skillRepo.UpsertBatch(userSkills) + totalCount += len(userSkills) } }