diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index e9113e9..5232eda 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -491,6 +491,7 @@ func main() { skillGroup.GET("/list", skillHandler.List) skillGroup.GET("/sync", skillHandler.Sync) skillGroup.GET("/:id", skillHandler.GetByID) + skillGroup.GET("/content", skillHandler.GetSkillContent) skillGroup.POST("/add", skillHandler.Create) skillGroup.PUT("/:id", skillHandler.Update) skillGroup.DELETE("/:id", skillHandler.Delete) diff --git a/server/internal/handler/skill_handler.go b/server/internal/handler/skill_handler.go index d1ec785..6f6d249 100644 --- a/server/internal/handler/skill_handler.go +++ b/server/internal/handler/skill_handler.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "net/http" "os" "path/filepath" @@ -234,6 +235,19 @@ func (h *SkillHandler) getProjectRoot() string { // @Param skill body model.Skill true "技能信息" // @Success 200 {object} map[string]interface{} "{"message": "skill updated"}" // @Router /skill/{id} [put] +// Update 更新技能 +// @Summary 更新技能 +// @Description 更新技能信息,支持文件上传 +// @Tags 技能管理 +// @Accept multipart/form-data +// @Produce json +// @Param id path string true "技能ID" +// @Param skill_name formData string false "技能名称" +// @Param skill_desc formData string false "技能描述" +// @Param skill_type formData string false "技能类型" +// @Param file formData file false "技能文件(SKILL.md)" +// @Success 200 {object} map[string]interface{} "{"message": "skill updated"}" +// @Router /skill/{id} [put] func (h *SkillHandler) Update(c *gin.Context) { id := c.Param("id") if id == "" { @@ -241,19 +255,142 @@ func (h *SkillHandler) Update(c *gin.Context) { return } - var skill model.Skill - if err := c.ShouldBindJSON(&skill); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + // 获取现有技能信息 + existingSkill, err := h.skillService.GetSkillByID(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "skill not found"}) return } - skill.ID = id - if err := h.skillService.UpdateSkill(&skill); err != nil { + skillName := c.PostForm("skill_name") + skillDesc := c.PostForm("skill_desc") + skillType := c.PostForm("skill_type") + + // 如果没有传则使用现有值 + if skillName == "" { + skillName = existingSkill.SkillName + } + if skillDesc == "" { + skillDesc = existingSkill.SkillDesc + } + if skillType == "" { + skillType = existingSkill.SkillType + } + + // 获取项目根目录 + projectRoot := h.getProjectRoot() + if projectRoot == "" { + c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot find project root"}) + return + } + + // 如果技能名称改变了,需要重命名文件夹 + oldPath := existingSkill.Path + if skillName != existingSkill.SkillName && oldPath != "" { + // 旧目录 + oldDir := filepath.Dir(oldPath) + // 新目录 + skillDir := "user" + if skillType == "system" { + skillDir = "system" + } + newDir := filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skillName) + + // 重命名目录 + if err := os.Rename(oldDir, newDir); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to rename skill directory: " + err.Error()}) + return + } + + // 更新路径 + existingSkill.Path = filepath.Join(newDir, "SKILL.md") + } + + // 处理文件上传 + file, err := c.FormFile("file") + if err == nil { + // 保存上传的文件为 SKILL.md + skillFilePath := filepath.Join(filepath.Dir(existingSkill.Path), "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 + } + } + + // 更新技能信息 + existingSkill.SkillName = skillName + existingSkill.SkillDesc = skillDesc + existingSkill.SkillType = skillType + + if err := h.skillService.UpdateSkill(existingSkill); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, gin.H{"message": "skill updated"}) + c.JSON(http.StatusOK, gin.H{"message": "skill updated", "skill": existingSkill}) +} + +// GetSkillContent 获取技能文件内容 +// @Summary 获取技能文件内容 +// @Description 获取指定技能对应的 SKILL.md 文件内容 +// @Tags 技能管理 +// @Accept json +// @Produce text/plain +// @Param id path string true "技能ID" +// @Success 200 {string} string "文件内容" +// @Router /skill/content [get] +func (h *SkillHandler) GetSkillContent(c *gin.Context) { + id := c.Query("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "skill id is required"}) + return + } + + // 获取技能信息 + skill, err := h.skillService.GetSkillByID(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "skill not found"}) + return + } + + // 确定文件路径 + filePath := skill.Path + if filePath == "" { + // 如果 path 为空,根据 skill_name 和 skill_type 构造默认路径 + projectRoot := h.getProjectRoot() + if projectRoot != "" { + skillDir := "user" + if skill.SkillType == "system" { + skillDir = "system" + } + filePath = filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skill.SkillName, "SKILL.md") + fmt.Printf("GetSkillContent: path is empty, constructed path: %s\n", filePath) + } + } + + // 如果仍然没有路径,返回错误 + if filePath == "" { + c.JSON(http.StatusNotFound, gin.H{"error": "skill path is empty"}) + return + } + + // 检查文件是否存在 + if _, err := os.Stat(filePath); os.IsNotExist(err) { + fmt.Printf("GetSkillContent error: file not found, path=%s\n", filePath) + c.JSON(http.StatusNotFound, gin.H{"error": "skill file not found: " + filePath}) + return + } + + // 读取文件内容 + content, err := os.ReadFile(filePath) + if err != nil { + fmt.Printf("GetSkillContent error: read file failed, path=%s, err=%s\n", filePath, err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"}) + return + } + + fmt.Printf("GetSkillContent success: id=%s, path=%s, size=%d\n", id, filePath, len(content)) + c.Data(http.StatusOK, "text/plain; charset=utf-8", content) } // Delete 删除技能