mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-09 12:54:47 +08:00
fix: unable to open filter in agent page(no agent tags...) (#16531)
### Summary As title
This commit is contained in:
@@ -18,6 +18,8 @@ package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -55,6 +57,60 @@ func userCanvasOrderClause(orderby string, desc bool) string {
|
||||
return orderby + " ASC"
|
||||
}
|
||||
|
||||
func userCanvasQualifiedOrderClause(orderby string, desc bool) string {
|
||||
if _, ok := userCanvasOrderableColumns[orderby]; !ok {
|
||||
orderby = "create_time"
|
||||
}
|
||||
order := "user_canvas." + orderby
|
||||
if desc {
|
||||
return order + " DESC"
|
||||
}
|
||||
return order + " ASC"
|
||||
}
|
||||
|
||||
func escapeSQLLike(s string) string {
|
||||
replacer := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
|
||||
return replacer.Replace(s)
|
||||
}
|
||||
|
||||
func splitUserCanvasTags(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
tags := make([]string, 0, len(parts))
|
||||
for _, tag := range parts {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag != "" {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func applyUserCanvasTagFilter(query *gorm.DB, tags []string) *gorm.DB {
|
||||
if len(tags) == 0 {
|
||||
return query
|
||||
}
|
||||
tagQuery := DB.Session(&gorm.Session{NewDB: true})
|
||||
hasTag := false
|
||||
for _, tag := range tags {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
pattern := "(^|,)[[:space:]]*" + regexp.QuoteMeta(tag) + "[[:space:]]*(,|$)"
|
||||
cond := DB.Where("user_canvas.tags REGEXP ?", pattern)
|
||||
if !hasTag {
|
||||
tagQuery = tagQuery.Where(cond)
|
||||
hasTag = true
|
||||
} else {
|
||||
tagQuery = tagQuery.Or(cond)
|
||||
}
|
||||
}
|
||||
if !hasTag {
|
||||
return query
|
||||
}
|
||||
return query.Where(tagQuery)
|
||||
}
|
||||
|
||||
var ErrUserCanvasNotFound = errors.New("user_canvas: not found or access denied")
|
||||
|
||||
// UserCanvasDAO user canvas data access object
|
||||
@@ -271,56 +327,129 @@ func (dao *UserCanvasDAO) GetAllCanvasesByTenantIDs(tenantIDs []string, userID s
|
||||
return results, err
|
||||
}
|
||||
|
||||
// UserCanvasListItem is the joined row returned by ListByTenantIDs.
|
||||
type UserCanvasListItem struct {
|
||||
ID string `gorm:"column:id"`
|
||||
Avatar *string `gorm:"column:avatar"`
|
||||
Title *string `gorm:"column:title"`
|
||||
Description *string `gorm:"column:description"`
|
||||
Permission string `gorm:"column:permission"`
|
||||
UserID string `gorm:"column:user_id"`
|
||||
TenantID string `gorm:"column:tenant_id"`
|
||||
Nickname *string `gorm:"column:nickname"`
|
||||
TenantAvatar *string `gorm:"column:tenant_avatar"`
|
||||
CanvasType *string `gorm:"column:canvas_type"`
|
||||
CanvasCategory string `gorm:"column:canvas_category"`
|
||||
Tags string `gorm:"column:tags"`
|
||||
CreateTime *int64 `gorm:"column:create_time"`
|
||||
UpdateTime *int64 `gorm:"column:update_time"`
|
||||
}
|
||||
|
||||
// ListByTenantIDs lists agent canvases accessible to the given owner IDs with optional
|
||||
// keyword filter, pagination, and ordering.
|
||||
// keyword filter, tag filter, pagination, and ordering.
|
||||
// Mirrors Python UserCanvasService.get_by_tenant_ids (list route only).
|
||||
func (dao *UserCanvasDAO) ListByTenantIDs(ownerIDs []string, userID string, page, pageSize int, orderby string, desc bool, keywords string, canvasCategory string) ([]*entity.UserCanvas, int64, error) {
|
||||
func (dao *UserCanvasDAO) ListByTenantIDs(ownerIDs []string, userID string, page, pageSize int, orderby string, desc bool, keywords string, canvasCategory string, tags []string) ([]*UserCanvasListItem, int64, error) {
|
||||
if len(ownerIDs) == 0 {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
// Canvases owned by any of the ownerIDs that are "team"-permission, plus all owned by userID.
|
||||
base := DB.Model(&entity.UserCanvas{}).
|
||||
Select(`user_canvas.id,
|
||||
user_canvas.avatar,
|
||||
user_canvas.title,
|
||||
user_canvas.description,
|
||||
user_canvas.permission,
|
||||
user_canvas.user_id,
|
||||
user_canvas.user_id AS tenant_id,
|
||||
user.nickname,
|
||||
user.avatar AS tenant_avatar,
|
||||
user_canvas.canvas_type,
|
||||
user_canvas.canvas_category,
|
||||
user_canvas.tags,
|
||||
user_canvas.create_time,
|
||||
user_canvas.update_time`).
|
||||
Joins("LEFT JOIN user ON user_canvas.user_id = user.id").
|
||||
Where(
|
||||
DB.Where("user_id IN ? AND permission = ?", ownerIDs, "team").
|
||||
Or("user_id = ?", userID),
|
||||
)
|
||||
DB.Where("user_canvas.user_id IN ? AND user_canvas.permission = ?", ownerIDs, "team").
|
||||
Or("user_canvas.user_id = ?", userID),
|
||||
"user_canvas.user_id IN ?",
|
||||
ownerIDs,
|
||||
).Where(
|
||||
DB.Where("user_canvas.permission = ?", "team").
|
||||
Or("user_canvas.user_id = ?", userID))
|
||||
|
||||
if canvasCategory != "" {
|
||||
base = base.Where("canvas_category = ?", canvasCategory)
|
||||
base = base.Where("user_canvas.canvas_category = ?", canvasCategory)
|
||||
} else {
|
||||
base = base.Where("canvas_category = ?", "agent_canvas")
|
||||
base = base.Where("user_canvas.canvas_category = ?", "agent_canvas")
|
||||
}
|
||||
|
||||
if keywords != "" {
|
||||
like := "%" + keywords + "%"
|
||||
base = base.Where("title LIKE ?", like)
|
||||
base = base.Where("user_canvas.title LIKE ?", like)
|
||||
}
|
||||
base = applyUserCanvasTagFilter(base, tags)
|
||||
|
||||
var total int64
|
||||
if err := base.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
order := userCanvasOrderClause(orderby, desc)
|
||||
order := userCanvasQualifiedOrderClause(orderby, desc)
|
||||
// codeql[go/sql-injection] False positive: `order` was just derived
|
||||
// from userCanvasOrderClause above, which validates `orderby`
|
||||
// from userCanvasQualifiedOrderClause above, which validates `orderby`
|
||||
// against userCanvasOrderableColumns (a closed allowlist) and
|
||||
// defaults to "create_time" on miss. The string spliced into
|
||||
// Order() is always one of a fixed set of column names.
|
||||
// Order() is always one of a fixed set of qualified column names.
|
||||
query := base.Order(order)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
query = query.Offset((page - 1) * pageSize).Limit(pageSize)
|
||||
}
|
||||
|
||||
var canvases []*entity.UserCanvas
|
||||
if err := query.Find(&canvases).Error; err != nil {
|
||||
var canvases []*UserCanvasListItem
|
||||
if err := query.Scan(&canvases).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return canvases, total, nil
|
||||
}
|
||||
|
||||
// ListTags returns tag usage counts across canvases visible to userID.
|
||||
func (dao *UserCanvasDAO) ListTags(ownerIDs []string, userID string, canvasCategory string) (map[string]int, error) {
|
||||
if len(ownerIDs) == 0 {
|
||||
return map[string]int{}, nil
|
||||
}
|
||||
|
||||
query := DB.Model(&entity.UserCanvas{}).
|
||||
Select("user_canvas.tags").
|
||||
Where(
|
||||
DB.Where("user_canvas.user_id IN ? AND user_canvas.permission = ?", ownerIDs, "team").
|
||||
Or("user_canvas.user_id = ?", userID),
|
||||
)
|
||||
|
||||
if canvasCategory != "" {
|
||||
query = query.Where("user_canvas.canvas_category = ?", canvasCategory)
|
||||
} else {
|
||||
query = query.Where("user_canvas.canvas_category = ?", "agent_canvas")
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
Tags string `gorm:"column:tags"`
|
||||
}
|
||||
if err := query.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counts := make(map[string]int)
|
||||
for _, row := range rows {
|
||||
for _, tag := range splitUserCanvasTags(row.Tags) {
|
||||
counts[tag]++
|
||||
}
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// GetByCanvasID get user canvas by canvas ID (alias for GetByID)
|
||||
func (dao *UserCanvasDAO) GetByCanvasID(canvasID string) (*entity.UserCanvas, error) {
|
||||
return dao.GetByID(canvasID)
|
||||
|
||||
@@ -167,6 +167,15 @@ func (h *AgentHandler) ListAgents(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
var tags []string
|
||||
if raw := c.Query("tags"); raw != "" {
|
||||
for _, tag := range strings.Split(raw, ",") {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag != "" {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result, code, err := h.agentService.ListAgents(
|
||||
user.ID,
|
||||
@@ -177,6 +186,7 @@ func (h *AgentHandler) ListAgents(c *gin.Context) {
|
||||
desc,
|
||||
ownerIDs,
|
||||
canvasCategory,
|
||||
tags,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -682,16 +692,23 @@ func (h *AgentHandler) Prompts(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ListAgentTags GET /api/v1/agents/tags — out of scope (no test depends on
|
||||
// it); return 501 to keep the surface honest.
|
||||
// ListAgentTags list agent tags.
|
||||
func (h *AgentHandler) ListAgentTags(c *gin.Context) {
|
||||
if _, code, msg := GetUser(c); code != common.CodeSuccess {
|
||||
user, code, msg := GetUser(c)
|
||||
if code != common.CodeSuccess {
|
||||
jsonError(c, code, msg)
|
||||
return
|
||||
}
|
||||
|
||||
rows, errCode, err := h.agentService.ListAgentTags(user.ID, strings.TrimSpace(c.Query("canvas_category")))
|
||||
if err != nil {
|
||||
jsonError(c, errCode, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": []string{},
|
||||
"data": rows,
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ type fakeAgentService struct {
|
||||
|
||||
// agentServiceIface is the minimum interface the handler depends on.
|
||||
type agentServiceIface interface {
|
||||
ListAgents(userID, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string, canvasCategory string) (*service.ListAgentsResponse, common.ErrorCode, error)
|
||||
ListAgents(userID, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string, canvasCategory string, tags []string) (*service.ListAgentsResponse, common.ErrorCode, error)
|
||||
ListTemplates() ([]*entity.CanvasTemplate, error)
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ func (h *agentHandlerTestable) listAgents(c *gin.Context) {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
result, code, err := h.svc.ListAgents(user.ID, "", 0, 0, "create_time", true, nil, "")
|
||||
result, code, err := h.svc.ListAgents(user.ID, "", 0, 0, "create_time", true, nil, "", nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": code, "data": false, "message": err.Error()})
|
||||
return
|
||||
@@ -359,7 +359,7 @@ func (h *agentHandlerTestable) listTemplates(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": templates, "message": "success"})
|
||||
}
|
||||
|
||||
func (f *fakeAgentService) ListAgents(userID, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string, canvasCategory string) (*service.ListAgentsResponse, common.ErrorCode, error) {
|
||||
func (f *fakeAgentService) ListAgents(userID, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string, canvasCategory string, tags []string) (*service.ListAgentsResponse, common.ErrorCode, error) {
|
||||
return f.result, f.code, f.err
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ type fullFakeAgentService struct {
|
||||
versions []*entity.UserCanvasVersion
|
||||
}
|
||||
|
||||
func (f *fullFakeAgentService) ListAgents(string, string, int, int, string, bool, []string, string) (*service.ListAgentsResponse, common.ErrorCode, error) {
|
||||
func (f *fullFakeAgentService) ListAgents(string, string, int, int, string, bool, []string, string, []string) (*service.ListAgentsResponse, common.ErrorCode, error) {
|
||||
return &service.ListAgentsResponse{}, common.CodeSuccess, nil
|
||||
}
|
||||
func (f *fullFakeAgentService) CreateAgent(context.Context, *service.CreateAgentRequest) (*entity.UserCanvas, common.ErrorCode, error) {
|
||||
|
||||
@@ -75,7 +75,7 @@ func newWaitFakeAgentService(stub func(call int, root map[string]any) (*runtime.
|
||||
}
|
||||
}
|
||||
|
||||
func (f *waitFakeAgentService) ListAgents(string, string, int, int, string, bool, []string, string) (*service.ListAgentsResponse, common.ErrorCode, error) {
|
||||
func (f *waitFakeAgentService) ListAgents(string, string, int, int, string, bool, []string, string, []string) (*service.ListAgentsResponse, common.ErrorCode, error) {
|
||||
return &service.ListAgentsResponse{}, common.CodeSuccess, nil
|
||||
}
|
||||
func (f *waitFakeAgentService) CreateAgent(context.Context, *service.CreateAgentRequest) (*entity.UserCanvas, common.ErrorCode, error) {
|
||||
|
||||
@@ -203,7 +203,13 @@ type AgentItem struct {
|
||||
ID string `json:"id"`
|
||||
Avatar *string `json:"avatar,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Permission string `json:"permission"`
|
||||
UserID string `json:"user_id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Nickname string `json:"nickname"`
|
||||
TenantAvatar *string `json:"tenant_avatar,omitempty"`
|
||||
Tags string `json:"tags"`
|
||||
CanvasType *string `json:"canvas_type,omitempty"`
|
||||
CanvasCategory string `json:"canvas_category"`
|
||||
CreateTime *int64 `json:"create_time,omitempty"`
|
||||
@@ -216,14 +222,32 @@ type ListAgentsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
func toAgentItem(c *entity.UserCanvas) *AgentItem {
|
||||
type AgentTagCount struct {
|
||||
Tag string `json:"tag"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func toAgentItem(c *dao.UserCanvasListItem) *AgentItem {
|
||||
nickname := ""
|
||||
if c.Nickname != nil {
|
||||
nickname = *c.Nickname
|
||||
}
|
||||
if nickname == "" {
|
||||
nickname = c.TenantID
|
||||
}
|
||||
return &AgentItem{
|
||||
ID: c.ID,
|
||||
Avatar: c.Avatar,
|
||||
Title: c.Title,
|
||||
Description: c.Description,
|
||||
Permission: c.Permission,
|
||||
UserID: c.UserID,
|
||||
TenantID: c.TenantID,
|
||||
Nickname: nickname,
|
||||
TenantAvatar: c.TenantAvatar,
|
||||
CanvasType: c.CanvasType,
|
||||
CanvasCategory: c.CanvasCategory,
|
||||
Tags: c.Tags,
|
||||
CreateTime: c.CreateTime,
|
||||
UpdateTime: c.UpdateTime,
|
||||
}
|
||||
@@ -232,7 +256,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, tags []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 {
|
||||
@@ -268,6 +292,7 @@ func (s *AgentService) ListAgents(userID string, keywords string, page, pageSize
|
||||
desc,
|
||||
keywords,
|
||||
canvasCategory,
|
||||
tags,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, fmt.Errorf("failed to list agents: %w", err)
|
||||
|
||||
@@ -511,6 +511,41 @@ func (s *AgentService) DeleteAgentSessions(userID, agentID string, ids []string,
|
||||
return &DeleteAgentSessionsResult{}, common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
// ListAgentTags list agent tags
|
||||
func (s *AgentService) ListAgentTags(userID, canvasCategory string) ([]AgentTagCount, common.ErrorCode, error) {
|
||||
tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, err
|
||||
}
|
||||
|
||||
ownerSet := make(map[string]struct{}, len(tenantIDs)+1)
|
||||
ownerSet[userID] = struct{}{}
|
||||
for _, id := range tenantIDs {
|
||||
ownerSet[id] = struct{}{}
|
||||
}
|
||||
ownerIDs := make([]string, 0, len(ownerSet))
|
||||
for id := range ownerSet {
|
||||
ownerIDs = append(ownerIDs, id)
|
||||
}
|
||||
|
||||
counts, err := s.canvasDAO.ListTags(ownerIDs, userID, canvasCategory)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, err
|
||||
}
|
||||
|
||||
tags := make([]AgentTagCount, 0, len(counts))
|
||||
for tag, count := range counts {
|
||||
tags = append(tags, AgentTagCount{Tag: tag, Count: count})
|
||||
}
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
if tags[i].Count == tags[j].Count {
|
||||
return tags[i].Tag < tags[j].Tag
|
||||
}
|
||||
return tags[i].Count > tags[j].Count
|
||||
})
|
||||
return tags, common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
// normalizeAgentTags returns an error for unsupported tag payload types.
|
||||
// The branch behaviour intentionally mirrors the Python implementation:
|
||||
// - string: treat the value as a CSV — split on "," and use each piece
|
||||
|
||||
Reference in New Issue
Block a user