Files
ragflow/internal/service/agent.go
Jack 23aae19898 feat: implement POST /api/v1/agents/<agent_id>/upload API (#15633)
## 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>
2026-06-04 17:21:47 +08:00

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
}