From 7795685f43fad896587b350aa365c12710a3653d Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Thu, 12 Mar 2026 17:16:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=90=8E=E7=AB=AFage?= =?UTF-8?q?nt=E5=92=8Cskill=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agent_handler.go: 新增ListAgents、CreateAgent接口 - skill_handler.go: 更新skill内容获取和保存功能 - agent_service.go: 新增agent服务逻辑 - main.go: 新增agent路由 Co-Authored-By: Claude Opus 4.6 --- server/cmd/api/main.go | 2 + server/internal/handler/agent_handler.go | 76 +++++++++++ server/internal/handler/skill_handler.go | 159 ++++++++++++++++++++--- server/internal/service/agent_service.go | 115 ++++++++++++++++ 4 files changed, 335 insertions(+), 17 deletions(-) diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 5232eda..051b80a 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -500,6 +500,8 @@ func main() { // Agent管理模块 (调用 Python Agent Engine) agentGroup := r.Group("/api/agent") { + agentGroup.GET("/list", agentHandler.ListAgents) + agentGroup.POST("/create", agentHandler.CreateAgent) agentGroup.POST("/chat", agentHandler.Chat) agentGroup.POST("/chat/stream", agentHandler.ChatStream) agentGroup.POST("/team/chat", agentHandler.TeamChat) diff --git a/server/internal/handler/agent_handler.go b/server/internal/handler/agent_handler.go index 98d1164..6133bbb 100644 --- a/server/internal/handler/agent_handler.go +++ b/server/internal/handler/agent_handler.go @@ -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) +} diff --git a/server/internal/handler/skill_handler.go b/server/internal/handler/skill_handler.go index 6f6d249..b98810b 100644 --- a/server/internal/handler/skill_handler.go +++ b/server/internal/handler/skill_handler.go @@ -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()}) diff --git a/server/internal/service/agent_service.go b/server/internal/service/agent_service.go index f9646ce..45100ef 100644 --- a/server/internal/service/agent_service.go +++ b/server/internal/service/agent_service.go @@ -279,3 +279,118 @@ func (s *AgentService) ChatStream(c interface{}, agentID int, message, sessionID return nil } + +// CreateAgentRequest 创建智能体请求 +type CreateAgentRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Avatar string `json:"avatar"` + SkillsMode string `json:"skills_mode"` + Skills []string `json:"skills"` + Knowledge string `json:"knowledge"` + Prompt string `json:"prompt"` + ModelProvider string `json:"model_provider"` + ModelName string `json:"model_name"` + UserID int `json:"user_id"` +} + +// CreateAgentResponse 创建智能体响应 +type CreateAgentResponse struct { + AgentID int `json:"agent_id"` + Name string `json:"name"` + Message string `json:"message"` +} + +// CreateAgent 创建智能体 +func (s *AgentService) CreateAgent(req CreateAgentRequest, userID int) (*CreateAgentResponse, error) { + url := fmt.Sprintf("%s/agent/create", s.pythonURL) + + // 构建请求体 + pythonReq := CreateAgentRequest{ + Name: req.Name, + Description: req.Description, + Avatar: req.Avatar, + SkillsMode: req.SkillsMode, + Skills: req.Skills, + Knowledge: req.Knowledge, + Prompt: req.Prompt, + ModelProvider: req.ModelProvider, + ModelName: req.ModelName, + UserID: userID, + } + + jsonData, err := json.Marshal(pythonReq) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := s.client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to call python agent: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("python agent error: %s", string(body)) + } + + var result CreateAgentResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + log.Printf("[AgentService] Agent created: %s (ID: %d)", result.Name, result.AgentID) + + return &result, nil +} + +// ListAgentsResponse 获取智能体列表响应 +type ListAgentsResponse struct { + Agents []interface{} `json:"agents"` +} + +// ListAgents 获取智能体列表 +func (s *AgentService) ListAgents() (*ListAgentsResponse, error) { + url := fmt.Sprintf("%s/agent/list", s.pythonURL) + + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := s.client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to call python agent: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("python agent error: %s", string(body)) + } + + var result ListAgentsResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + log.Printf("[AgentService] Listed agents: %d", len(result.Agents)) + + return &result, nil +}