package handler import ( "fmt" "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 技能处理器 type SkillHandler struct { skillService *service.SkillService } // NewSkillHandler 创建技能处理器 func NewSkillHandler(skillService *service.SkillService) *SkillHandler { return &SkillHandler{skillService: skillService} } // List 获取技能列表 // @Summary 获取技能列表 // @Description 获取所有技能列表,支持按类型筛选(system/user) // @Tags 技能管理 // @Accept json // @Produce json // @Param type query string false "技能类型: system(系统技能)/user(用户技能)" // @Success 200 {object} map[string]interface{} "{"list": [], "total": 0}" // @Router /skill/list [get] func (h *SkillHandler) List(c *gin.Context) { skillType := c.Query("type") var skills []model.Skill var err error if skillType != "" { skills, err = h.skillService.GetSkillsByType(skillType) } else { skills, err = h.skillService.GetAllSkills() } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"list": skills, "total": len(skills)}) } // Sync 手动同步 skills // @Summary 手动同步技能 // @Description 从文件系统扫描 skills 目录并同步到数据库。扫描 account/admin/skills(系统技能) 和 account/{username}/skills(用户技能) // @Tags 技能管理 // @Accept json // @Produce json // @Success 200 {object} map[string]interface{} "{"message": "skills synced", "count": 0}" // @Router /skill/sync [get] func (h *SkillHandler) Sync(c *gin.Context) { if err := h.skillService.InitSkills(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } skills, _ := h.skillService.GetAllSkills() c.JSON(http.StatusOK, gin.H{"message": "skills synced", "count": len(skills)}) } // GetByID 根据ID获取技能 // @Summary 获取技能详情 // @Description 根据ID获取技能详情 // @Tags 技能管理 // @Accept json // @Produce json // @Param id path string true "技能ID" // @Success 200 {object} map[string]interface{} "{"skill": {}}" // @Router /skill/{id} [get] func (h *SkillHandler) GetByID(c *gin.Context) { id := c.Param("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 } c.JSON(http.StatusOK, gin.H{"skill": skill}) } // Create 创建技能 // @Summary 创建技能 // @Description 创建新的技能,支持文件上传。管理员用户(admin)上传为system技能,存到core/agents/skills/system/;其他用户上传为user技能,存到core/agents/skills/user/ // @Tags 技能管理 // @Accept multipart/form-data // @Produce json // @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) { // 获取当前用户信息 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 } // 确定技能类型:优先使用传入的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 } 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 更新技能信息 // @Tags 技能管理 // @Accept json // @Produce json // @Param id path string true "技能ID" // @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 == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "skill id is required"}) return } // 获取现有技能信息 existingSkill, err := h.skillService.GetSkillByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "skill not found"}) return } 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", "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 删除技能 // @Summary 删除技能 // @Description 删除技能 // @Tags 技能管理 // @Accept json // @Produce json // @Param id path string true "技能ID" // @Success 200 {object} map[string]interface{} "{"message": "skill deleted"}" // @Router /skill/{id} [delete] func (h *SkillHandler) Delete(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "skill id is required"}) return } if err := h.skillService.DeleteSkill(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "skill deleted"}) }