feat[Go]: implement Agent/Workflow PUT /api/v1/agents/<canvas_id>/tags (#15641)

feat[Go]: implement Agent/Workflow PUT /api/v1/agents/<canvas_id>/tags (#15641)
This commit is contained in:
Haruko386
2026-06-05 13:22:23 +08:00
committed by GitHub
parent 71649db3b0
commit 4b2af1347c
6 changed files with 221 additions and 10 deletions

View File

@@ -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
}

View File

@@ -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"`

View File

@@ -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",
})
}

View File

@@ -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 }

View File

@@ -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")

View File

@@ -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) {