mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-07 03:48:44 +08:00
## Summary Implement the `POST /api/v1/agents/<agent_id>/upload` endpoint in Go, allowing file uploads associated with agent canvases. ### Changes - **Modified**: `internal/service/agent.go` — Added `CheckCanvasAccess` method (owner + team-level permission semantics) - **Modified**: `internal/handler/agent.go` — Added `UploadAgentFile` handler with auth check, multipart file parsing, and delegation to `FileService`. Added `fileUploader` interface for testability. - **Modified**: `internal/router/router.go` — Registered `POST /:agent_id/upload` route - **Modified**: `cmd/server_main.go` — Wired `fileService` into `AgentHandler` - **New**: `internal/service/agent_test.go` — 4 service-level tests for `CheckCanvasAccess` (owner, team member, private denial, not found) - **New**: `internal/handler/agent_upload_test.go` — 3 handler-level tests (success with fake file service, cross-user denial, empty file rejection) ### Testing All 7 tests pass with zero mocking of the DB layer (in-memory SQLite): ``` === RUN TestCheckCanvasAccess_Owner --- PASS === RUN TestCheckCanvasAccess_NotOwner --- PASS === RUN TestCheckCanvasAccess_PrivateCanvas_Denied --- PASS === RUN TestCheckCanvasAccess_NotFound --- PASS === RUN TestUploadAgentFileHandler_Success --- PASS === RUN TestUploadAgentFileHandler_NoPermission --- PASS === RUN TestUploadAgentFileHandler_NoFiles --- PASS ``` Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
176 lines
5.1 KiB
Go
176 lines
5.1 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
package service
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"ragflow/internal/common"
|
|
"ragflow/internal/dao"
|
|
"ragflow/internal/entity"
|
|
)
|
|
|
|
// AgentService agent service
|
|
type AgentService struct {
|
|
canvasDAO *dao.UserCanvasDAO
|
|
userTenantDAO *dao.UserTenantDAO
|
|
userCanvasVersionDAO *dao.UserCanvasVersionDAO
|
|
}
|
|
|
|
// NewAgentService create agent service
|
|
func NewAgentService() *AgentService {
|
|
return &AgentService{
|
|
canvasDAO: dao.NewUserCanvasDAO(),
|
|
userTenantDAO: dao.NewUserTenantDAO(),
|
|
userCanvasVersionDAO: dao.NewUserCanvasVersionDAO(),
|
|
}
|
|
}
|
|
|
|
// AgentItem is one entry in the list response.
|
|
type AgentItem struct {
|
|
ID string `json:"id"`
|
|
Avatar *string `json:"avatar,omitempty"`
|
|
Title *string `json:"title,omitempty"`
|
|
Permission string `json:"permission"`
|
|
CanvasType *string `json:"canvas_type,omitempty"`
|
|
CanvasCategory string `json:"canvas_category"`
|
|
CreateTime *int64 `json:"create_time,omitempty"`
|
|
UpdateTime *int64 `json:"update_time,omitempty"`
|
|
}
|
|
|
|
// ListAgentsResponse is the response body for GET /api/v1/agents.
|
|
type ListAgentsResponse struct {
|
|
Canvas []*AgentItem `json:"canvas"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
func toAgentItem(c *entity.UserCanvas) *AgentItem {
|
|
return &AgentItem{
|
|
ID: c.ID,
|
|
Avatar: c.Avatar,
|
|
Title: c.Title,
|
|
Permission: c.Permission,
|
|
CanvasType: c.CanvasType,
|
|
CanvasCategory: c.CanvasCategory,
|
|
CreateTime: c.CreateTime,
|
|
UpdateTime: c.UpdateTime,
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
// Build the set of tenant IDs the user is authorised to query.
|
|
tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(userID)
|
|
if err != nil {
|
|
return nil, common.CodeServerError, fmt.Errorf("failed to get tenant IDs: %w", err)
|
|
}
|
|
authorised := make(map[string]struct{}, len(tenantIDs)+1)
|
|
for _, id := range tenantIDs {
|
|
authorised[id] = struct{}{}
|
|
}
|
|
authorised[userID] = struct{}{}
|
|
|
|
var effectiveOwnerIDs []string
|
|
if len(ownerIDs) > 0 {
|
|
for _, id := range ownerIDs {
|
|
if _, ok := authorised[id]; !ok {
|
|
return nil, common.CodeOperatingError, fmt.Errorf("only authorized owner_ids can be queried")
|
|
}
|
|
}
|
|
effectiveOwnerIDs = ownerIDs
|
|
} else {
|
|
effectiveOwnerIDs = make([]string, 0, len(authorised))
|
|
for id := range authorised {
|
|
effectiveOwnerIDs = append(effectiveOwnerIDs, id)
|
|
}
|
|
}
|
|
|
|
canvases, total, err := s.canvasDAO.ListByTenantIDs(
|
|
effectiveOwnerIDs,
|
|
userID,
|
|
page,
|
|
pageSize,
|
|
orderby,
|
|
desc,
|
|
keywords,
|
|
canvasCategory,
|
|
)
|
|
if err != nil {
|
|
return nil, common.CodeServerError, fmt.Errorf("failed to list agents: %w", err)
|
|
}
|
|
|
|
items := make([]*AgentItem, len(canvases))
|
|
for i, c := range canvases {
|
|
items[i] = toAgentItem(c)
|
|
}
|
|
return &ListAgentsResponse{Canvas: items, Total: total}, 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) {
|
|
canvas, err := s.canvasDAO.GetByID(canvasID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
// Owner always has access
|
|
if canvas.UserID == userID {
|
|
return true, nil
|
|
}
|
|
// Non-owner: only team-level permission grants tenant access
|
|
if canvas.Permission != string(entity.TenantPermissionTeam) {
|
|
return false, nil
|
|
}
|
|
tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(userID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, tid := range tenantIDs {
|
|
if canvas.UserID == tid {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// ListVersions returns all versions for an agent canvas, ordered by update_time DESC.
|
|
func (s *AgentService) ListVersions(canvasID string) ([]*entity.UserCanvasVersion, error) {
|
|
return s.userCanvasVersionDAO.ListByCanvasID(canvasID)
|
|
}
|
|
|
|
// GetVersion returns a specific version by ID, verifying it belongs to the given canvas.
|
|
func (s *AgentService) GetVersion(canvasID, versionID string) (*entity.UserCanvasVersion, error) {
|
|
version, err := s.userCanvasVersionDAO.GetByID(versionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if version.UserCanvasID != canvasID {
|
|
return nil, fmt.Errorf("version not found")
|
|
}
|
|
return version, nil
|
|
}
|