mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-15 01:18:26 +08:00
## Summary Port the canvas-template catalogue endpoint to the Go API server. Listed in the Go-API port checklist of #15240. Mirrors `list_agent_template` in `api/apps/restful_apis/agent_api.py`: returns every row from the `canvas_template` table so that the UI can render the template gallery on the New-Agent screen. ## What - `internal/dao/canvas_template.go` — new `CanvasTemplateDAO.GetAll()` ordered by `create_time desc` (newest templates first). - `internal/service/agent.go` — wire the new DAO into `AgentService` and expose `ListTemplates() ([]*entity.CanvasTemplate, error)`. - `internal/handler/agent.go` — new `AgentHandler.ListTemplates` HTTP handler (auth-gated, mirrors Python `@login_required`). - `internal/router/router.go` — `agents.GET("/templates", r.agentHandler.ListTemplates)` registered alongside the existing `GET /agents`. - `internal/handler/agent_test.go` — three new tests covering: success path, empty-list → JSON array (not `null`), and the auth gate. ## Notes - `CanvasTemplate` entity, GORM tags, and DB migration already exist in `internal/entity/canvas.go` and `internal/dao/database.go` — no schema change required. - The handler coerces a `nil` slice to `[]*entity.CanvasTemplate{}` so the JSON payload is always an array (the frontend does `data.map(...)` on it). ## Test plan - [x] `go vet ./internal/handler ./internal/service ./internal/dao ./internal/router` clean - [x] Three unit tests added; existing `TestListAgents_Success` untouched - [ ] CI runs `go test ./internal/handler` with cgo binding linked ## Related - Tracker: #15240
This commit is contained in:
40
internal/dao/canvas_template.go
Normal file
40
internal/dao/canvas_template.go
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// 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 dao
|
||||
|
||||
import (
|
||||
"ragflow/internal/entity"
|
||||
)
|
||||
|
||||
// CanvasTemplateDAO data-access object for the canvas_template table.
|
||||
type CanvasTemplateDAO struct{}
|
||||
|
||||
// NewCanvasTemplateDAO creates a CanvasTemplate DAO.
|
||||
func NewCanvasTemplateDAO() *CanvasTemplateDAO {
|
||||
return &CanvasTemplateDAO{}
|
||||
}
|
||||
|
||||
// GetAll returns every row in canvas_template ordered by create_time desc, so
|
||||
// templates appear newest first in the UI. Mirrors the Python
|
||||
// CanvasTemplateService.get_all() behaviour.
|
||||
func (dao *CanvasTemplateDAO) GetAll() ([]*entity.CanvasTemplate, error) {
|
||||
var templates []*entity.CanvasTemplate
|
||||
if err := DB.Order("create_time desc").Find(&templates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return templates, nil
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/entity"
|
||||
"ragflow/internal/service"
|
||||
)
|
||||
|
||||
@@ -182,6 +183,37 @@ func (h *AgentHandler) ListAgentVersions(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ListTemplates lists every canvas template available to authenticated users.
|
||||
// @Summary List Agent Templates
|
||||
// @Description List the catalogue of canvas templates that authenticated users can clone.
|
||||
// @Tags agents
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/v1/agents/templates [get]
|
||||
func (h *AgentHandler) ListTemplates(c *gin.Context) {
|
||||
if _, errorCode, errorMessage := GetUser(c); errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
templates, err := h.agentService.ListTemplates()
|
||||
if err != nil {
|
||||
jsonError(c, common.CodeServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if templates == nil {
|
||||
// Ensure the JSON payload is always a list, never null.
|
||||
templates = []*entity.CanvasTemplate{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": templates,
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// UploadAgentFile uploads one or more files associated with an agent.
|
||||
// @Summary Upload Agent File
|
||||
// @Description Upload one or more files for an agent canvas.
|
||||
@@ -309,7 +341,7 @@ func (h *AgentHandler) UploadAgentFile(c *gin.Context) {
|
||||
uploaded, err := h.fileService.UploadFile(user.ID, "", files)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeOperatingError,
|
||||
"code": common.CodeOperatingError,
|
||||
|
||||
"data": nil,
|
||||
"message": err.Error(),
|
||||
@@ -318,8 +350,8 @@ func (h *AgentHandler) UploadAgentFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": uploaded,
|
||||
"code": common.CodeSuccess,
|
||||
"data": uploaded,
|
||||
|
||||
"message": "",
|
||||
})
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ragflow/internal/common"
|
||||
@@ -195,6 +195,7 @@ func TestListAgentVersionsHandler_CanvasNotFound(t *testing.T) {
|
||||
t.Errorf("expected operating error code %d, got %v", common.CodeOperatingError, code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAgentVersionHandler_Success verifies getting a specific version.
|
||||
func TestGetAgentVersionHandler_Success(t *testing.T) {
|
||||
c, w, db := setupGinContextWithUserAndDB(t, "GET", "/api/v1/agents/canvas-1/versions/v1")
|
||||
@@ -260,8 +261,191 @@ func TestGetAgentVersionHandler_VersionNotFound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// sptr returns a pointer to the given string.
|
||||
// ptr returns a pointer to the given int64.
|
||||
func ptr(v int64) *int64 { return &v }
|
||||
|
||||
// fakeAgentService satisfies the subset of AgentService used by the handler.
|
||||
// It is injected via a wrapper to avoid importing the real DAO (which requires a DB).
|
||||
type fakeAgentService struct {
|
||||
result *service.ListAgentsResponse
|
||||
code common.ErrorCode
|
||||
err error
|
||||
templates []*entity.CanvasTemplate
|
||||
templatesErr error
|
||||
}
|
||||
|
||||
// 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)
|
||||
ListTemplates() ([]*entity.CanvasTemplate, error)
|
||||
}
|
||||
|
||||
// agentHandlerTestable is a version of AgentHandler that accepts the interface.
|
||||
type agentHandlerTestable struct {
|
||||
svc agentServiceIface
|
||||
}
|
||||
|
||||
func (h *agentHandlerTestable) listAgents(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
result, code, err := h.svc.ListAgents(user.ID, "", 0, 0, "create_time", true, nil, "")
|
||||
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": result, "message": "success"})
|
||||
}
|
||||
|
||||
func (h *agentHandlerTestable) listTemplates(c *gin.Context) {
|
||||
if _, errorCode, errorMessage := GetUser(c); errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
templates, err := h.svc.ListTemplates()
|
||||
if err != nil {
|
||||
jsonError(c, common.CodeServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if templates == nil {
|
||||
templates = []*entity.CanvasTemplate{}
|
||||
}
|
||||
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) {
|
||||
return f.result, f.code, f.err
|
||||
}
|
||||
|
||||
func (f *fakeAgentService) ListTemplates() ([]*entity.CanvasTemplate, error) {
|
||||
return f.templates, f.templatesErr
|
||||
}
|
||||
|
||||
func setupAgentRouter(svc agentServiceIface) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
h := &agentHandlerTestable{svc: svc}
|
||||
r.GET("/api/v1/agents", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "user-abc"})
|
||||
h.listAgents(c)
|
||||
})
|
||||
r.GET("/api/v1/agents/templates", func(c *gin.Context) {
|
||||
c.Set("user", &entity.User{ID: "user-abc"})
|
||||
h.listTemplates(c)
|
||||
})
|
||||
r.GET("/api/v1/agents/templates_anon", func(c *gin.Context) {
|
||||
// no user set → unauthenticated probe
|
||||
h.listTemplates(c)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func TestListAgents_Success(t *testing.T) {
|
||||
title := "My Agent"
|
||||
svc := &fakeAgentService{
|
||||
result: &service.ListAgentsResponse{
|
||||
Canvas: []*service.AgentItem{{ID: "canvas-1", Title: &title, Permission: "me", CanvasCategory: "agent_canvas"}},
|
||||
Total: 1,
|
||||
},
|
||||
code: common.CodeSuccess,
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/agents", nil)
|
||||
setupAgentRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["code"] != float64(common.CodeSuccess) {
|
||||
t.Errorf("expected code %d, got %v", common.CodeSuccess, body["code"])
|
||||
}
|
||||
data, ok := body["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("data is not a map: %v", body["data"])
|
||||
}
|
||||
if data["total"] != float64(1) {
|
||||
t.Errorf("expected total=1, got %v", data["total"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAgentTemplates_Success(t *testing.T) {
|
||||
cnvType := "agent"
|
||||
svc := &fakeAgentService{
|
||||
templates: []*entity.CanvasTemplate{
|
||||
{
|
||||
ID: "template-1",
|
||||
CanvasType: &cnvType,
|
||||
CanvasCategory: "agent_canvas",
|
||||
Title: entity.JSONMap{"en": "Sample"},
|
||||
Description: entity.JSONMap{"en": "Sample desc"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/agents/templates", nil)
|
||||
setupAgentRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v body=%s", err, w.Body.String())
|
||||
}
|
||||
if body["code"] != float64(common.CodeSuccess) {
|
||||
t.Errorf("code=%v want %d", body["code"], common.CodeSuccess)
|
||||
}
|
||||
data, ok := body["data"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("data is not an array: %v", body["data"])
|
||||
}
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected 1 template, got %d", len(data))
|
||||
}
|
||||
first := data[0].(map[string]interface{})
|
||||
if first["id"] != "template-1" {
|
||||
t.Errorf("id=%v want template-1", first["id"])
|
||||
}
|
||||
if first["canvas_category"] != "agent_canvas" {
|
||||
t.Errorf("canvas_category=%v want agent_canvas", first["canvas_category"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAgentTemplates_EmptyIsArrayNotNull(t *testing.T) {
|
||||
svc := &fakeAgentService{templates: nil}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/agents/templates", nil)
|
||||
setupAgentRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
var body map[string]interface{}
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &body)
|
||||
// JSON shape contract: never null - frontends do .map() on it.
|
||||
if _, ok := body["data"].([]interface{}); !ok {
|
||||
t.Fatalf("data is not an array when templates empty: %v (raw=%s)", body["data"], w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAgentTemplates_RequiresAuth(t *testing.T) {
|
||||
svc := &fakeAgentService{templates: []*entity.CanvasTemplate{}}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/agents/templates_anon", nil)
|
||||
setupAgentRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if code, _ := body["code"].(float64); int(code) == int(common.CodeSuccess) {
|
||||
t.Errorf("expected non-success without auth, got body=%v", body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,26 +23,26 @@ import (
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
authHandler *handler.AuthHandler
|
||||
userHandler *handler.UserHandler
|
||||
tenantHandler *handler.TenantHandler
|
||||
documentHandler *handler.DocumentHandler
|
||||
datasetsHandler *handler.DatasetsHandler
|
||||
systemHandler *handler.SystemHandler
|
||||
knowledgebaseHandler *handler.KnowledgebaseHandler
|
||||
chunkHandler *handler.ChunkHandler
|
||||
llmHandler *handler.LLMHandler
|
||||
chatHandler *handler.ChatHandler
|
||||
chatSessionHandler *handler.ChatSessionHandler
|
||||
connectorHandler *handler.ConnectorHandler
|
||||
searchHandler *handler.SearchHandler
|
||||
fileHandler *handler.FileHandler
|
||||
memoryHandler *handler.MemoryHandler
|
||||
mcpHandler *handler.MCPHandler
|
||||
skillSearchHandler *handler.SkillSearchHandler
|
||||
providerHandler *handler.ProviderHandler
|
||||
agentHandler *handler.AgentHandler
|
||||
relatedQuestionsHandler *handler.SearchbotHandler
|
||||
authHandler *handler.AuthHandler
|
||||
userHandler *handler.UserHandler
|
||||
tenantHandler *handler.TenantHandler
|
||||
documentHandler *handler.DocumentHandler
|
||||
datasetsHandler *handler.DatasetsHandler
|
||||
systemHandler *handler.SystemHandler
|
||||
knowledgebaseHandler *handler.KnowledgebaseHandler
|
||||
chunkHandler *handler.ChunkHandler
|
||||
llmHandler *handler.LLMHandler
|
||||
chatHandler *handler.ChatHandler
|
||||
chatSessionHandler *handler.ChatSessionHandler
|
||||
connectorHandler *handler.ConnectorHandler
|
||||
searchHandler *handler.SearchHandler
|
||||
fileHandler *handler.FileHandler
|
||||
memoryHandler *handler.MemoryHandler
|
||||
mcpHandler *handler.MCPHandler
|
||||
skillSearchHandler *handler.SkillSearchHandler
|
||||
providerHandler *handler.ProviderHandler
|
||||
agentHandler *handler.AgentHandler
|
||||
relatedQuestionsHandler *handler.SearchbotHandler
|
||||
}
|
||||
|
||||
// NewRouter create router
|
||||
@@ -69,25 +69,25 @@ func NewRouter(
|
||||
relatedQuestionsHandler *handler.SearchbotHandler,
|
||||
) *Router {
|
||||
return &Router{
|
||||
authHandler: authHandler,
|
||||
userHandler: userHandler,
|
||||
tenantHandler: tenantHandler,
|
||||
documentHandler: documentHandler,
|
||||
datasetsHandler: datasetsHandler,
|
||||
systemHandler: systemHandler,
|
||||
knowledgebaseHandler: knowledgebaseHandler,
|
||||
chunkHandler: chunkHandler,
|
||||
llmHandler: llmHandler,
|
||||
chatHandler: chatHandler,
|
||||
chatSessionHandler: chatSessionHandler,
|
||||
connectorHandler: connectorHandler,
|
||||
searchHandler: searchHandler,
|
||||
fileHandler: fileHandler,
|
||||
memoryHandler: memoryHandler,
|
||||
mcpHandler: mcpHandler,
|
||||
skillSearchHandler: skillSearchHandler,
|
||||
providerHandler: providerHandler,
|
||||
agentHandler: agentHandler,
|
||||
authHandler: authHandler,
|
||||
userHandler: userHandler,
|
||||
tenantHandler: tenantHandler,
|
||||
documentHandler: documentHandler,
|
||||
datasetsHandler: datasetsHandler,
|
||||
systemHandler: systemHandler,
|
||||
knowledgebaseHandler: knowledgebaseHandler,
|
||||
chunkHandler: chunkHandler,
|
||||
llmHandler: llmHandler,
|
||||
chatHandler: chatHandler,
|
||||
chatSessionHandler: chatSessionHandler,
|
||||
connectorHandler: connectorHandler,
|
||||
searchHandler: searchHandler,
|
||||
fileHandler: fileHandler,
|
||||
memoryHandler: memoryHandler,
|
||||
mcpHandler: mcpHandler,
|
||||
skillSearchHandler: skillSearchHandler,
|
||||
providerHandler: providerHandler,
|
||||
agentHandler: agentHandler,
|
||||
relatedQuestionsHandler: relatedQuestionsHandler,
|
||||
}
|
||||
}
|
||||
@@ -374,6 +374,7 @@ func (r *Router) Setup(engine *gin.Engine) {
|
||||
agents := v1.Group("/agents")
|
||||
{
|
||||
agents.GET("", r.agentHandler.ListAgents)
|
||||
agents.GET("/templates", r.agentHandler.ListTemplates)
|
||||
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)
|
||||
|
||||
@@ -26,9 +26,10 @@ import (
|
||||
|
||||
// AgentService agent service
|
||||
type AgentService struct {
|
||||
canvasDAO *dao.UserCanvasDAO
|
||||
userTenantDAO *dao.UserTenantDAO
|
||||
canvasDAO *dao.UserCanvasDAO
|
||||
userTenantDAO *dao.UserTenantDAO
|
||||
userCanvasVersionDAO *dao.UserCanvasVersionDAO
|
||||
canvasTemplateDAO *dao.CanvasTemplateDAO
|
||||
}
|
||||
|
||||
// NewAgentService create agent service
|
||||
@@ -37,9 +38,17 @@ func NewAgentService() *AgentService {
|
||||
canvasDAO: dao.NewUserCanvasDAO(),
|
||||
userTenantDAO: dao.NewUserTenantDAO(),
|
||||
userCanvasVersionDAO: dao.NewUserCanvasVersionDAO(),
|
||||
canvasTemplateDAO: dao.NewCanvasTemplateDAO(),
|
||||
}
|
||||
}
|
||||
|
||||
// ListTemplates returns every canvas template. Mirrors Python
|
||||
// agent_api.list_agent_template, which iterates CanvasTemplateService.get_all()
|
||||
// and serialises each row.
|
||||
func (s *AgentService) ListTemplates() ([]*entity.CanvasTemplate, error) {
|
||||
return s.canvasTemplateDAO.GetAll()
|
||||
}
|
||||
|
||||
// AgentItem is one entry in the list response.
|
||||
type AgentItem struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
Reference in New Issue
Block a user