From 4b2af1347c21499346dbf3ae953cb5414615971a Mon Sep 17 00:00:00 2001 From: Haruko386 Date: Fri, 5 Jun 2026 13:22:23 +0800 Subject: [PATCH] feat[Go]: implement Agent/Workflow PUT /api/v1/agents//tags (#15641) feat[Go]: implement Agent/Workflow PUT /api/v1/agents//tags (#15641) --- internal/dao/user_canvas.go | 6 ++ internal/entity/canvas.go | 1 + internal/handler/agent.go | 38 ++++++++++++ internal/handler/agent_test.go | 74 ++++++++++++++++++++++ internal/router/router.go | 2 +- internal/service/agent.go | 110 ++++++++++++++++++++++++++++++--- 6 files changed, 221 insertions(+), 10 deletions(-) diff --git a/internal/dao/user_canvas.go b/internal/dao/user_canvas.go index a333c7775b..f73cc0f9e9 100644 --- a/internal/dao/user_canvas.go +++ b/internal/dao/user_canvas.go @@ -200,3 +200,9 @@ func (dao *UserCanvasDAO) GetAllCanvasIDsByUserID(userID string) ([]string, erro Pluck("id", &canvasIDs).Error return canvasIDs, err } + +// UpdateTags updates a canvas's comma-separated tags by canvas ID. +func (dao *UserCanvasDAO) UpdateTags(canvasID, tags string) (int64, error) { + result := DB.Model(&entity.UserCanvas{}).Where("id = ?", canvasID).Update("tags", tags) + return result.RowsAffected, result.Error +} diff --git a/internal/entity/canvas.go b/internal/entity/canvas.go index fe2124dfd3..c9c9310856 100644 --- a/internal/entity/canvas.go +++ b/internal/entity/canvas.go @@ -21,6 +21,7 @@ type UserCanvas struct { ID string `gorm:"column:id;primaryKey;size:32" json:"id"` Avatar *string `gorm:"column:avatar;type:longtext" json:"avatar,omitempty"` UserID string `gorm:"column:user_id;size:255;not null;index" json:"user_id"` + Tags string `gorm:"column:tags;size:512;not null;default:'';index" json:"tags"` Title *string `gorm:"column:title;size:255" json:"title,omitempty"` Permission string `gorm:"column:permission;size:16;not null;default:me;index" json:"permission"` Release bool `gorm:"column:release;not null;default:false;index" json:"release"` diff --git a/internal/handler/agent.go b/internal/handler/agent.go index 15df6a0fdd..db35ba192b 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -356,3 +356,41 @@ func (h *AgentHandler) UploadAgentFile(c *gin.Context) { "message": "", }) } + +type updateAgentTagsRequest struct { + Tags interface{} `json:"tags"` +} + +func (h *AgentHandler) UpdateAgentTags(c *gin.Context) { + user, errorCode, errorMessage := GetUser(c) + if errorCode != common.CodeSuccess { + jsonError(c, errorCode, errorMessage) + return + } + + var req updateAgentTagsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusOK, gin.H{ + "code": common.CodeBadRequest, + "data": false, + "message": err.Error(), + }) + return + } + + data, code, err := h.agentService.UpdateAgentTags(user.ID, c.Param("agent_id"), req.Tags) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "code": code, + "data": false, + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": common.CodeSuccess, + "data": data, + "message": "success", + }) +} diff --git a/internal/handler/agent_test.go b/internal/handler/agent_test.go index f18f214517..c1c15aca13 100644 --- a/internal/handler/agent_test.go +++ b/internal/handler/agent_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -261,6 +262,79 @@ func TestGetAgentVersionHandler_VersionNotFound(t *testing.T) { } } +func TestUpdateAgentTagsHandlerSuccess(t *testing.T) { + c, w, db := setupGinContextWithUserAndDB(t, http.MethodPut, "/api/v1/agents/canvas-1/tags") + c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/agents/canvas-1/tags", strings.NewReader(`{"tags":["alpha","beta","alpha"]}`)) + c.Request.Header.Set("Content-Type", "application/json") + c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}} + + db.Create(&entity.UserCanvas{ + ID: "canvas-1", + UserID: "user-1", + Title: sptr("Test Agent"), + }) + + h := NewAgentHandler(service.NewAgentService(), nil) + h.UpdateAgentTags(c) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to parse response: %v", err) + } + code, _ := resp["code"].(float64) + if code != float64(common.CodeSuccess) { + t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, code, resp["message"]) + } + if resp["data"] != true { + t.Fatalf("expected data true, got %v", resp["data"]) + } + + var canvas entity.UserCanvas + if err := db.Where("id = ?", "canvas-1").First(&canvas).Error; err != nil { + t.Fatalf("failed to reload canvas: %v", err) + } + if canvas.Tags != "alpha,beta" { + t.Fatalf("expected normalized tags alpha,beta, got %q", canvas.Tags) + } +} + +func TestUpdateAgentTagsHandlerNoPermission(t *testing.T) { + c, w, db := setupGinContextWithUserAndDB(t, http.MethodPut, "/api/v1/agents/canvas-b/tags") + c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/agents/canvas-b/tags", strings.NewReader(`{"tags":["alpha"]}`)) + c.Request.Header.Set("Content-Type", "application/json") + c.Params = gin.Params{{Key: "agent_id", Value: "canvas-b"}} + + db.Create(&entity.UserCanvas{ + ID: "canvas-b", + UserID: "user-b", + Title: sptr("Private Agent"), + Permission: "me", + }) + + h := NewAgentHandler(service.NewAgentService(), nil) + h.UpdateAgentTags(c) + + var resp map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to parse response: %v", err) + } + code, _ := resp["code"].(float64) + if code != float64(common.CodeOperatingError) { + t.Fatalf("expected code %d, got %v: %v", common.CodeOperatingError, code, resp["message"]) + } + if resp["data"] != false { + t.Fatalf("expected data false, got %v", resp["data"]) + } + if resp["message"] != "Agent not found or no permission." { + t.Fatalf("unexpected message: %v", resp["message"]) + } +} + +// sptr returns a pointer to the given string. // ptr returns a pointer to the given int64. func ptr(v int64) *int64 { return &v } diff --git a/internal/router/router.go b/internal/router/router.go index 9a84c7b55c..4a249f381d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -378,7 +378,7 @@ func (r *Router) Setup(engine *gin.Engine) { agents.GET("/:agent_id/versions", r.agentHandler.ListAgentVersions) agents.GET("/:agent_id/versions/:version_id", r.agentHandler.GetAgentVersion) agents.POST("/:agent_id/upload", r.agentHandler.UploadAgentFile) - + agents.PUT("/:agent_id/tags", r.agentHandler.UpdateAgentTags) } connector := v1.Group("/connectors") diff --git a/internal/service/agent.go b/internal/service/agent.go index c62a89b5e2..3eb284c7d7 100644 --- a/internal/service/agent.go +++ b/internal/service/agent.go @@ -18,12 +18,18 @@ package service import ( "fmt" + "strings" "ragflow/internal/common" "ragflow/internal/dao" "ragflow/internal/entity" ) +const ( + agentTagsFieldMax = 512 + agentTagMaxLen = 64 +) + // AgentService agent service type AgentService struct { canvasDAO *dao.UserCanvasDAO @@ -83,15 +89,7 @@ func toAgentItem(c *entity.UserCanvas) *AgentItem { // ListAgents returns agent canvases visible to userID. // Mirrors Python agent_api.list_agents — validates owner_ids against joined tenants, // then delegates to the DAO. -func (s *AgentService) ListAgents( - userID string, - keywords string, - page, pageSize int, - orderby string, - desc bool, - ownerIDs []string, - canvasCategory string, -) (*ListAgentsResponse, common.ErrorCode, error) { +func (s *AgentService) ListAgents(userID string, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string, canvasCategory string) (*ListAgentsResponse, common.ErrorCode, error) { // Build the set of tenant IDs the user is authorised to query. tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(userID) if err != nil { @@ -139,6 +137,100 @@ func (s *AgentService) ListAgents( return &ListAgentsResponse{Canvas: items, Total: total}, common.CodeSuccess, nil } +// normalizeAgentTags returns an error for unsupported tag payload types +func normalizeAgentTags(rawTags interface{}) (string, error) { + cleaned := make([]string, 0) + switch tags := rawTags.(type) { + case nil: + case string: + for _, tag := range strings.Split(tags, ",") { + tag = strings.TrimSpace(tag) + if tag != "" { + cleaned = append(cleaned, tag) + } + } + case []string: + for _, tag := range tags { + tag = strings.TrimSpace(strings.ReplaceAll(tag, ",", " ")) + if tag != "" { + cleaned = append(cleaned, tag) + } + } + case []interface{}: + for _, value := range tags { + if value == nil { + continue + } + tag := strings.TrimSpace(strings.ReplaceAll(fmt.Sprint(value), ",", " ")) + if tag != "" { + cleaned = append(cleaned, tag) + } + } + default: + return "", fmt.Errorf("tags must be a string or array") + } + + seen := make(map[string]struct{}, len(cleaned)) + normalized := make([]string, 0, len(cleaned)) + used := 0 + for _, tag := range cleaned { + tag = truncateRunes(tag, agentTagMaxLen) + key := strings.ToLower(tag) + if _, ok := seen[key]; ok { + continue + } + + extra := len([]rune(tag)) + if len(normalized) > 0 { + extra++ + } + if used+extra > agentTagsFieldMax { + break + } + + seen[key] = struct{}{} + normalized = append(normalized, tag) + used += extra + } + + return strings.Join(normalized, ","), nil +} + +func truncateRunes(value string, maxLen int) string { + runes := []rune(value) + if len(runes) <= maxLen { + return value + } + return string(runes[:maxLen]) +} + +func (s *AgentService) UpdateAgentTags(userID, canvasID string, tags interface{}) (bool, common.ErrorCode, error) { + ok, err := s.CheckCanvasAccess(userID, canvasID) + if err != nil { + return false, common.CodeServerError, fmt.Errorf("failed to check agent permission: %w", err) + } + if !ok { + return false, common.CodeOperatingError, fmt.Errorf("Agent not found or no permission.") + } + + normalized, nErr := normalizeAgentTags(tags) + if nErr != nil { + return false, common.CodeBadRequest, nErr + } + rows, err := s.canvasDAO.UpdateTags(canvasID, normalized) + if err != nil { + return false, common.CodeServerError, fmt.Errorf("failed to update agent tags: %w", err) + } + if rows == 0 { + if _, getErr := s.canvasDAO.GetByCanvasID(canvasID); getErr != nil { + return false, common.CodeOperatingError, fmt.Errorf("Agent not found or no permission.") + } + return true, common.CodeSuccess, nil + } + + return true, common.CodeSuccess, nil +} + // CheckCanvasAccess checks if a user has access to a canvas. // Returns true if the user is the owner or has team-level permission. func (s *AgentService) CheckCanvasAccess(userID, canvasID string) (bool, error) {