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>
This commit is contained in:
Jack
2026-06-04 17:21:47 +08:00
committed by GitHub
parent b65b18ba4c
commit 23aae19898
7 changed files with 334 additions and 10 deletions

View File

@@ -17,6 +17,8 @@
package handler
import (
"fmt"
"mime/multipart"
"net/http"
"strconv"
"strings"
@@ -27,14 +29,21 @@ import (
"ragflow/internal/service"
)
// AgentHandler agent handler
// fileUploader is the subset of FileService used by agent handlers.
type fileUploader interface {
UploadFile(tenantID, parentID string, files []*multipart.FileHeader) ([]map[string]interface{}, error)
}
// AgentHandler agent handler
type AgentHandler struct {
agentService *service.AgentService
fileService fileUploader
}
// NewAgentHandler create agent handler
func NewAgentHandler(agentService *service.AgentService) *AgentHandler {
return &AgentHandler{agentService: agentService}
func NewAgentHandler(agentService *service.AgentService, fileService *service.FileService) *AgentHandler {
return &AgentHandler{agentService: agentService, fileService: fileService}
}
// ListAgents lists agent canvases for the current user.
@@ -169,3 +178,81 @@ func (h *AgentHandler) ListAgentVersions(c *gin.Context) {
"message": "",
})
}
// UploadAgentFile uploads one or more files associated with an agent.
// @Summary Upload Agent File
// @Description Upload one or more files for an agent canvas.
// @Tags agents
// @Accept multipart/form-data
// @Produce json
// @Param agent_id path string true "Agent ID"
// @Param file formData file true "File(s) to upload (multiple files supported)"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/agents/{agent_id}/upload [post]
func (h *AgentHandler) UploadAgentFile(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
agentID := c.Param("agent_id")
if agentID == "" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeArgumentError,
"data": nil,
"message": "agent_id is required",
})
return
}
ok, err := h.agentService.CheckCanvasAccess(user.ID, agentID)
if err != nil || !ok {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeOperatingError,
"data": nil,
"message": "Agent not found or no permission.",
})
return
}
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeArgumentError,
"data": nil,
"message": fmt.Sprintf("invalid form data: %v", err),
})
return
}
files := form.File["file"]
if len(files) == 0 {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeArgumentError,
"data": nil,
"message": "You have to upload at least one file.",
})
return
}
// Use the canvas owner's tenant ID for file ownership.
uploaded, err := h.fileService.UploadFile(user.ID, "", files)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeOperatingError,
"data": nil,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": common.CodeSuccess,
"data": uploaded,
"message": "",
})
}