diff --git a/internal/handler/agent.go b/internal/handler/agent.go index f280d15160..0e32eaec0c 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -17,6 +17,7 @@ package handler import ( + "errors" "fmt" "mime/multipart" "net/http" @@ -24,6 +25,8 @@ import ( "strings" "github.com/gin-gonic/gin" + "go.uber.org/zap" + "gorm.io/gorm" "ragflow/internal/common" "ragflow/internal/service" @@ -182,6 +185,71 @@ func (h *AgentHandler) ListAgentVersions(c *gin.Context) { // UploadAgentFile uploads one or more files associated with an agent. // @Summary Upload Agent File // @Description Upload one or more files for an agent canvas. + +// GetAgentVersion returns a specific version for an agent. +// @Summary Get Agent Version +// @Description Returns a specific version by ID, verifying it belongs to the given agent. +// @Tags agents +// @Produce json +// @Param agent_id path string true "Agent ID" +// @Param version_id path string true "Version ID" +// @Success 200 {object} map[string]interface{} +// @Router /api/v1/agents/{agent_id}/versions/{version_id} [get] +func (h *AgentHandler) GetAgentVersion(c *gin.Context) { + user, errorCode, errorMessage := GetUser(c) + if errorCode != common.CodeSuccess { + jsonError(c, errorCode, errorMessage) + return + } + + agentID := c.Param("agent_id") + versionID := c.Param("version_id") + if agentID == "" || versionID == "" { + c.JSON(http.StatusOK, gin.H{ + "code": common.CodeArgumentError, + "data": nil, + "message": "agent_id and version_id are 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 + } + + version, err := h.agentService.GetVersion(agentID, versionID) + if err != nil { + isNotFound := errors.Is(err, gorm.ErrRecordNotFound) || err.Error() == "version not found" + if !isNotFound { + common.Warn("get agent version failed", zap.String("error", err.Error())) + c.JSON(http.StatusOK, gin.H{ + "code": common.CodeServerError, + "data": nil, + "message": "Internal server error", + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "code": common.CodeNotFound, + "data": nil, + "message": "Version not found.", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": common.CodeSuccess, + "data": version, + "message": "", + }) +} + // @Tags agents // @Accept multipart/form-data // @Produce json diff --git a/internal/handler/agent_test.go b/internal/handler/agent_test.go index f264913eb7..6e07ab4088 100644 --- a/internal/handler/agent_test.go +++ b/internal/handler/agent_test.go @@ -195,6 +195,73 @@ 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") + c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "version_id", Value: "v1"}} + + db.Create(&entity.UserCanvas{ + ID: "canvas-1", + UserID: "user-1", + Title: sptr("Test Agent"), + }) + db.Create(&entity.UserCanvasVersion{ + ID: "v1", + UserCanvasID: "canvas-1", + Title: sptr("version-1"), + DSL: entity.JSONMap{"key": "value"}, + }) + + h := NewAgentHandler(service.NewAgentService(), nil) + h.GetAgentVersion(c) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + code, _ := resp["code"].(float64) + if code != float64(common.CodeSuccess) { + t.Fatalf("expected code 0, got %v: %v", code, resp["message"]) + } + + data, ok := resp["data"].(map[string]interface{}) + if !ok { + t.Fatalf("expected data object, got %T", resp["data"]) + } + if data["title"] != "version-1" { + t.Errorf("expected title 'version-1', got %v", data["title"]) + } + if _, ok := data["dsl"]; !ok { + t.Errorf("expected dsl field in version detail response") + } +} + +// TestGetAgentVersionHandler_VersionNotFound verifies 404 for missing version. +func TestGetAgentVersionHandler_VersionNotFound(t *testing.T) { + c, w, db := setupGinContextWithUserAndDB(t, "GET", "/api/v1/agents/canvas-1/versions/non-existent") + c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "version_id", Value: "non-existent"}} + + db.Create(&entity.UserCanvas{ + ID: "canvas-1", + UserID: "user-1", + Title: sptr("Test Agent"), + }) + + h := NewAgentHandler(service.NewAgentService(), nil) + h.GetAgentVersion(c) + + var resp map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &resp) + code, _ := resp["code"].(float64) + if code != float64(common.CodeNotFound) { + t.Errorf("expected not found code %d, got %v", common.CodeNotFound, code) + } +} + + + // sptr returns a pointer to the given string. // ptr returns a pointer to the given int64. func ptr(v int64) *int64 { return &v } diff --git a/internal/router/router.go b/internal/router/router.go index a88cb923ed..ea50bca71e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -369,6 +369,7 @@ func (r *Router) Setup(engine *gin.Engine) { { agents.GET("", r.agentHandler.ListAgents) 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) } diff --git a/internal/service/agent_test.go b/internal/service/agent_test.go index 84ec62b652..07ba83a25f 100644 --- a/internal/service/agent_test.go +++ b/internal/service/agent_test.go @@ -262,5 +262,87 @@ func TestCheckCanvasAccess_NotFound(t *testing.T) { } } +// TestGetVersion_Success verifies getting a specific version by ID. +func TestGetVersion_Success(t *testing.T) { + testDB := setupServiceTestDB(t) + t.Helper() + + if err := testDB.AutoMigrate( + &entity.UserCanvasVersion{}, + ); err != nil { + t.Fatalf("failed to migrate: %v", err) + } + + orig := dao.DB + dao.DB = testDB + t.Cleanup(func() { dao.DB = orig }) + + testDB.Create(&entity.UserCanvasVersion{ + ID: "v1", + UserCanvasID: "canvas-1", + Title: sptr("version-1"), + DSL: entity.JSONMap{"model": "gpt-4"}, + }) + + svc := NewAgentService() + v, err := svc.GetVersion("canvas-1", "v1") + if err != nil { + t.Fatalf("GetVersion failed: %v", err) + } + if *v.Title != "version-1" { + t.Errorf("expected title 'version-1', got %s", *v.Title) + } +} + +// TestGetVersion_WrongCanvas verifies version belonging to another canvas returns error. +func TestGetVersion_WrongCanvas(t *testing.T) { + testDB := setupServiceTestDB(t) + t.Helper() + + if err := testDB.AutoMigrate( + &entity.UserCanvasVersion{}, + ); err != nil { + t.Fatalf("failed to migrate: %v", err) + } + + orig := dao.DB + dao.DB = testDB + t.Cleanup(func() { dao.DB = orig }) + + testDB.Create(&entity.UserCanvasVersion{ + ID: "v1", + UserCanvasID: "canvas-1", + Title: sptr("version-1"), + }) + + svc := NewAgentService() + _, err := svc.GetVersion("canvas-other", "v1") + if err == nil { + t.Error("expected error for version belonging to another canvas") + } +} + +// TestGetVersion_NotFound verifies error for non-existent version. +func TestGetVersion_NotFound(t *testing.T) { + testDB := setupServiceTestDB(t) + t.Helper() + + if err := testDB.AutoMigrate( + &entity.UserCanvasVersion{}, + ); err != nil { + t.Fatalf("failed to migrate: %v", err) + } + + orig := dao.DB + dao.DB = testDB + t.Cleanup(func() { dao.DB = orig }) + + svc := NewAgentService() + _, err := svc.GetVersion("canvas-1", "non-existent") + if err == nil { + t.Error("expected error for non-existent version") + } +} + // ptr returns a pointer to the given int64. func ptr(v int64) *int64 { return &v } \ No newline at end of file diff --git a/web/vite.config.ts b/web/vite.config.ts index b59a515116..60771cda8a 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -97,7 +97,7 @@ export default defineConfig(({ mode }) => { changeOrigin: true, ws: true, }, - '^(/api/v1/users)|^(/api/v1/auth)|^(/api/v1/users/me)|^(/api/v1/system/config)|^(/api/v1/system/version)|^(/api/v1/tenants)|^(/api/v1/chats)|^(/api/v1/searches)|^(/api/v1/files)|^(/api/v1/agents$)|^(/api/v1/agents/[^/]+/versions$)|^(/api/v1/agents/[^/]+/upload$)': + '^(/api/v1/users)|^(/api/v1/auth)|^(/api/v1/users/me)|^(/api/v1/system/config)|^(/api/v1/system/version)|^(/api/v1/tenants)|^(/api/v1/chats)|^(/api/v1/searches)|^(/api/v1/files)|^(/api/v1/agents$)|^(/api/v1/agents/[^/]+/versions$)|^(/api/v1/agents/[^/]+/versions/[^/]+$)': { target: 'http://127.0.0.1:9384/', changeOrigin: true,