feat: 更新后端agent和skill服务

- agent_handler.go: 新增ListAgents、CreateAgent接口
- skill_handler.go: 更新skill内容获取和保存功能
- agent_service.go: 新增agent服务逻辑
- main.go: 新增agent路由

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 17:16:53 +08:00
parent 249e2c2e9c
commit 7795685f43
4 changed files with 335 additions and 17 deletions

View File

@@ -40,7 +40,27 @@ type ChatResponse struct {
Metadata interface{} `json:"metadata"`
}
// CreateAgentResponse 创建智能体响应
type CreateAgentResponse struct {
AgentID int `json:"agent_id"`
Name string `json:"name"`
Message string `json:"message"`
}
// ListAgentsResponse 获取智能体列表响应
type ListAgentsResponse struct {
Agents []interface{} `json:"agents"`
}
// Chat 单智能体对话
// @Summary 单智能体对话
// @Tags 智能体管理
// @Accept json
// @Produce json
// @Param request body ChatRequest true "对话请求"
// @Success 200 {object} ChatResponse
// @Router /api/agent/chat [post]
func (h *AgentHandler) Chat(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
@@ -88,6 +108,12 @@ func (h *AgentHandler) Chat(c *gin.Context) {
}
// ChatStream 单智能体对话(流式输出)
// @Summary 单智能体对话(流式输出)
// @Tags 智能体管理
// @Accept json
// @Produce text/event-stream
// @Param request body ChatRequest true "对话请求"
// @Router /api/agent/chat/stream [post]
func (h *AgentHandler) ChatStream(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
@@ -132,6 +158,13 @@ type TeamChatResponse struct {
}
// TeamChat 多智能体群聊
// @Summary 多智能体群聊
// @Tags 智能体管理
// @Accept json
// @Produce json
// @Param request body TeamChatRequest true "群聊请求"
// @Success 200 {object} TeamChatResponse
// @Router /api/agent/team/chat [post]
func (h *AgentHandler) TeamChat(c *gin.Context) {
var req TeamChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
@@ -167,3 +200,46 @@ func (h *AgentHandler) TeamChat(c *gin.Context) {
Metadata: nil,
})
}
// CreateAgent 创建智能体
// @Summary 创建智能体
// @Tags 智能体管理
// @Accept json
// @Produce json
// @Param request body CreateAgentRequest true "创建智能体请求"
// @Success 200 {object} CreateAgentResponse
// @Router /api/agent/create [post]
func (h *AgentHandler) CreateAgent(c *gin.Context) {
var req service.CreateAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 获取用户 ID
userID := 1 // TODO: 从 c.Get("user_id") 获取
result, err := h.agentService.CreateAgent(req, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// ListAgents 获取智能体列表
// @Summary 获取智能体列表
// @Tags 智能体管理
// @Produce json
// @Success 200 {object} ListAgentsResponse
// @Router /api/agent/list [get]
func (h *AgentHandler) ListAgents(c *gin.Context) {
result, err := h.agentService.ListAgents()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}

View File

@@ -2,9 +2,12 @@ package handler
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"x-agents/server/internal/model"
"x-agents/server/internal/service"
@@ -115,18 +118,17 @@ func (h *SkillHandler) Create(c *gin.Context) {
}
userID, _ := c.Get("userID")
// skillName 和 skillDesc 改为可选,如果没传则从文件内容解析
// 先从前端获取,但如果上传了文件则以文件内容解析的为准
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
}
// 记录原始输入(用于调试)
originalSkillName := skillName
// 确定技能类型优先使用传入的type否则根据用户角色判断
if skillType == "" {
// 判断是否为管理员用户
if username == "admin" {
skillType = "system"
} else {
@@ -147,24 +149,87 @@ func (h *SkillHandler) Create(c *gin.Context) {
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
}
// 用于存储最终路径
var skillPath string
// 处理文件上传
file, err := c.FormFile("file")
if err == nil {
// 保存上传的文件为 SKILL.md
// 读取文件内容,解析 YAML front matter 获取 name
fileContent, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file: " + err.Error()})
return
}
defer fileContent.Close()
content := make([]byte, 1024*1024) // 最多读取 1MB
n, _ := fileContent.Read(content)
contentStr := string(content[:n])
// 解析 name
parsedName, parsedDesc := parseSkillMeta(contentStr)
log.Printf("[SkillHandler] Original skill_name from form: %s, parsed name from file: %s", originalSkillName, parsedName)
// 优先使用文件解析出的 name
if parsedName != "" {
skillName = parsedName
} else if skillName == "" {
// 如果解析不到且表单也没传,用文件名
skillName = filepath.Base(file.Filename)
}
if parsedDesc != "" {
skillDesc = parsedDesc
}
// 清理 skillName只保留纯文件名去除所有路径
skillName = filepath.Base(skillName)
// 去除 .md 后缀
skillName = strings.TrimSuffix(skillName, ".md")
// 去除空格
skillName = strings.TrimSpace(skillName)
// 如果包含路径分隔符,取最后一部分
if idx := strings.LastIndexAny(skillName, "/\\"); idx >= 0 {
skillName = skillName[idx+1:]
}
log.Printf("[SkillHandler] Final skill name: %s", skillName)
// 创建技能目录
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
}
// 保存文件(使用之前读取的内容)
skillFilePath := filepath.Join(skillPath, "SKILL.md")
if err := c.SaveUploadedFile(file, skillFilePath); err != nil {
if err := os.WriteFile(skillFilePath, []byte(contentStr), 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save skill file: " + err.Error()})
return
}
} else {
// 如果没有上传文件,创建一个默认的 SKILL.md
// 如果没有上传文件但提供了 name 和 desc创建默认文件
if skillName == "" || skillDesc == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill_name and skill_desc are required when no file uploaded"})
return
}
// 清理 skillName
skillName = filepath.Base(skillName)
skillName = strings.TrimSuffix(skillName, ".md")
skillName = strings.TrimSpace(skillName)
if idx := strings.LastIndexAny(skillName, "/\\"); idx >= 0 {
skillName = skillName[idx+1:]
}
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
}
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 {
@@ -196,6 +261,45 @@ func (h *SkillHandler) Create(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "skill created", "skill": skill})
}
// parseSkillMeta 解析 SKILL.md 内容,提取 name 和 description
func parseSkillMeta(content string) (name string, description string) {
contentStr := strings.TrimSpace(content)
if !strings.HasPrefix(contentStr, "---") {
return "", ""
}
// 找到第二个 ---
lines := strings.Split(contentStr, "\n")
var endIdx int
for i := 1; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) == "---" {
endIdx = i
break
}
}
if endIdx == 0 {
return "", ""
}
// 提取 YAML 内容
yamlContent := strings.Join(lines[1:endIdx], "\n")
// 解析 name
nameMatch := regexp.MustCompile(`name:\s*(.+)`).FindStringSubmatch(yamlContent)
if len(nameMatch) > 1 {
name = strings.TrimSpace(nameMatch[1])
}
// 解析 description
descMatch := regexp.MustCompile(`description:\s*(.+)`).FindStringSubmatch(yamlContent)
if len(descMatch) > 1 {
description = strings.TrimSpace(descMatch[1])
}
return name, description
}
// getProjectRoot 获取项目根目录
func (h *SkillHandler) getProjectRoot() string {
execPath, _ := os.Getwd()
@@ -262,11 +366,28 @@ func (h *SkillHandler) Update(c *gin.Context) {
return
}
skillName := c.PostForm("skill_name")
skillDesc := c.PostForm("skill_desc")
skillType := c.PostForm("skill_type")
// 支持 JSON 格式的请求
var req struct {
SkillName string `json:"skill_name"`
SkillDesc string `json:"skill_desc"`
SkillType string `json:"skill_type"`
Status string `json:"status"`
}
// 尝试解析 JSON如果失败则用 PostForm
if err := c.ShouldBindJSON(&req); err != nil {
req.SkillName = c.PostForm("skill_name")
req.SkillDesc = c.PostForm("skill_desc")
req.SkillType = c.PostForm("skill_type")
req.Status = c.PostForm("status")
}
// 如果没有传则使用现有值
skillName := req.SkillName
skillDesc := req.SkillDesc
skillType := req.SkillType
status := req.Status
if skillName == "" {
skillName = existingSkill.SkillName
}
@@ -276,6 +397,9 @@ func (h *SkillHandler) Update(c *gin.Context) {
if skillType == "" {
skillType = existingSkill.SkillType
}
if status == "" {
status = existingSkill.Status
}
// 获取项目根目录
projectRoot := h.getProjectRoot()
@@ -321,6 +445,7 @@ func (h *SkillHandler) Update(c *gin.Context) {
existingSkill.SkillName = skillName
existingSkill.SkillDesc = skillDesc
existingSkill.SkillType = skillType
existingSkill.Status = status
if err := h.skillService.UpdateSkill(existingSkill); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})