mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user