Files
X-Agents/server/internal/handler/skill_handler.go
DESKTOP-72TV0V4\caoxiaozhu 030b21949b feat: 新增获取skill内容的API路由
- 新增 GET /skill/content 获取skill的SKILL.md内容

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:42:33 +08:00

419 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"})
}