diff --git a/internal/bootstrap/routes.go b/internal/bootstrap/routes.go index f58a785..dea7d30 100644 --- a/internal/bootstrap/routes.go +++ b/internal/bootstrap/routes.go @@ -224,6 +224,19 @@ func registerDashboardAIAgentRoutes(group *gin.RouterGroup) { group.POST("/update_status", dashboard.AIAgentPostUpdate_status) } +func registerDashboardAIWorkflowRoutes(group *gin.RouterGroup) { + group.GET("/:id", dashboard.AIWorkflowGetBy) + group.Any("/list", dashboard.AIWorkflowAnyList) + group.POST("/create", dashboard.AIWorkflowPostCreate) + group.POST("/update", dashboard.AIWorkflowPostUpdate) + group.POST("/delete", dashboard.AIWorkflowPostDelete) + group.GET("/node-spec/list", dashboard.AIWorkflowGetNodeSpecList) + group.POST("/validate", dashboard.AIWorkflowPostValidate) + group.POST("/publish", dashboard.AIWorkflowPostPublish) + group.Any("/version/list", dashboard.AIWorkflowAnyVersionList) + group.GET("/version/:id", dashboard.AIWorkflowGetVersionBy) +} + func registerDashboardAIConfigRoutes(group *gin.RouterGroup) { group.GET("/:id", dashboard.AIConfigGetBy) group.POST("/create", dashboard.AIConfigPostCreate) diff --git a/internal/bootstrap/server.go b/internal/bootstrap/server.go index 6a6c55e..ba43f95 100644 --- a/internal/bootstrap/server.go +++ b/internal/bootstrap/server.go @@ -186,6 +186,7 @@ func addRouter(app *gin.Engine) { registerDashboardAgentTeamRoutes(dashboardGroup.Group("/agent-team")) registerDashboardAgentTeamScheduleRoutes(dashboardGroup.Group("/agent-team-schedule")) registerDashboardAIAgentRoutes(dashboardGroup.Group("/ai-agent")) + registerDashboardAIWorkflowRoutes(dashboardGroup.Group("/ai-workflow")) registerDashboardAIConfigRoutes(dashboardGroup.Group("/ai-config")) registerDashboardAssetRoutes(dashboardGroup.Group("/asset")) registerDashboardKnowledgeBaseRoutes(dashboardGroup.Group("/knowledge-base")) diff --git a/internal/handlers/dashboard/ai_workflow_handler.go b/internal/handlers/dashboard/ai_workflow_handler.go new file mode 100644 index 0000000..f64a703 --- /dev/null +++ b/internal/handlers/dashboard/ai_workflow_handler.go @@ -0,0 +1,174 @@ +package dashboard + +import ( + "agent-desk/internal/builders" + "agent-desk/internal/pkg/constants" + "agent-desk/internal/pkg/dto/request" + "agent-desk/internal/pkg/dto/response" + "agent-desk/internal/pkg/httpx" + "agent-desk/internal/pkg/httpx/params" + "agent-desk/internal/services" + + "github.com/gin-gonic/gin" + "github.com/mlogclub/simple/web" +) + +func AIWorkflowAnyList(ctx *gin.Context) { + if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { + httpx.WriteJSON(ctx, err) + return + } + cnd := params.NewPagedSqlCnd(ctx, + params.QueryFilter{ParamName: "status"}, + params.QueryFilter{ParamName: "name", Op: params.Like}, + params.QueryFilter{ParamName: "ownerType"}, + params.QueryFilter{ParamName: "ownerId"}, + ).Desc("id") + list, paging := services.AIWorkflowService.FindPageByCnd(cnd) + httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildAIWorkflowList(list), Page: paging}) +} + +func AIWorkflowGetBy(ctx *gin.Context) { + id, ok := httpx.GetPathInt64(ctx, "id") + if !ok { + return + } + if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { + httpx.WriteJSON(ctx, err) + return + } + item := services.AIWorkflowService.Get(id) + if item == nil { + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002")) + return + } + httpx.WriteJSON(ctx, builders.BuildAIWorkflow(item)) +} + +func AIWorkflowPostCreate(ctx *gin.Context) { + operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentCreate) + if err != nil { + httpx.WriteJSON(ctx, err) + return + } + req := request.CreateAIWorkflowRequest{} + if err := params.ReadJSON(ctx, &req); err != nil { + httpx.WriteJSON(ctx, err) + return + } + item, err := services.AIWorkflowService.CreateWorkflow(req, operator) + if err != nil { + httpx.WriteJSON(ctx, err) + return + } + httpx.WriteJSON(ctx, builders.BuildAIWorkflow(item)) +} + +func AIWorkflowPostUpdate(ctx *gin.Context) { + operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate) + if err != nil { + httpx.WriteJSON(ctx, err) + return + } + req := request.UpdateAIWorkflowRequest{} + if err := params.ReadJSON(ctx, &req); err != nil { + httpx.WriteJSON(ctx, err) + return + } + if err := services.AIWorkflowService.UpdateWorkflow(req, operator); err != nil { + httpx.WriteJSON(ctx, err) + return + } + httpx.WriteJSON(ctx, nil) +} + +func AIWorkflowPostDelete(ctx *gin.Context) { + operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentDelete) + if err != nil { + httpx.WriteJSON(ctx, err) + return + } + req := request.DeleteAIWorkflowRequest{} + if err := params.ReadJSON(ctx, &req); err != nil { + httpx.WriteJSON(ctx, err) + return + } + if err := services.AIWorkflowService.DeleteWorkflow(req.ID, operator); err != nil { + httpx.WriteJSON(ctx, err) + return + } + httpx.WriteJSON(ctx, nil) +} + +func AIWorkflowGetNodeSpecList(ctx *gin.Context) { + if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { + httpx.WriteJSON(ctx, err) + return + } + httpx.WriteJSON(ctx, builders.BuildAIWorkflowNodeSpecs(services.AIWorkflowService.ListNodeSpecs())) +} + +func AIWorkflowPostValidate(ctx *gin.Context) { + if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { + httpx.WriteJSON(ctx, err) + return + } + req := request.ValidateAIWorkflowRequest{} + if err := params.ReadJSON(ctx, &req); err != nil { + httpx.WriteJSON(ctx, err) + return + } + result := services.AIWorkflowService.ValidateDefinition(req.Definition) + httpx.WriteJSON(ctx, response.AIWorkflowValidationResponse{ + Valid: result.Valid, + Errors: result.Errors, + }) +} + +func AIWorkflowPostPublish(ctx *gin.Context) { + operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate) + if err != nil { + httpx.WriteJSON(ctx, err) + return + } + req := request.PublishAIWorkflowRequest{} + if err := params.ReadJSON(ctx, &req); err != nil { + httpx.WriteJSON(ctx, err) + return + } + item, err := services.AIWorkflowService.PublishWorkflow(req, operator) + if err != nil { + httpx.WriteJSON(ctx, err) + return + } + httpx.WriteJSON(ctx, builders.BuildAIWorkflowVersion(item)) +} + +func AIWorkflowAnyVersionList(ctx *gin.Context) { + if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { + httpx.WriteJSON(ctx, err) + return + } + queryParams := params.NewQueryParams(ctx) + cnd := params.NewPagedSqlCnd(ctx, params.QueryFilter{ParamName: "workflowId"}).Desc("version").Desc("id") + queryParams.Cnd = *cnd + list, paging := services.AIWorkflowService.FindVersionPageByParams(queryParams) + httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildAIWorkflowVersionList(list), Page: paging}) +} + +func AIWorkflowGetVersionBy(ctx *gin.Context) { + id, ok := httpx.GetPathInt64(ctx, "id") + if !ok { + return + } + if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil { + httpx.WriteJSON(ctx, err) + return + } + item := services.AIWorkflowService.GetVersion(id) + if item == nil { + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002")) + return + } + httpx.WriteJSON(ctx, builders.BuildAIWorkflowVersion(item)) +} diff --git a/internal/services/ai_workflow_service_test.go b/internal/services/ai_workflow_service_test.go index 497466b..6903043 100644 --- a/internal/services/ai_workflow_service_test.go +++ b/internal/services/ai_workflow_service_test.go @@ -8,6 +8,7 @@ import ( "agent-desk/internal/models" "agent-desk/internal/pkg/dto" "agent-desk/internal/pkg/dto/request" + "agent-desk/internal/repositories" "github.com/glebarez/sqlite" "github.com/mlogclub/simple/sqls" @@ -115,6 +116,43 @@ func TestAIWorkflowServicePublishIncrementsVersion(t *testing.T) { } } +func TestAIWorkflowServicePublishRejectsInvalidDSL(t *testing.T) { + setupAIWorkflowTestDB(t) + operator := aiWorkflowTestOperator() + workflow, err := AIWorkflowService.CreateWorkflow(request.CreateAIWorkflowRequest{ + Name: "invalid publish flow", + OwnerType: "ai_agent", + OwnerID: 23, + Definition: validAIWorkflowDefinition(), + }, operator) + if err != nil { + t.Fatalf("CreateWorkflow() error = %v", err) + } + + _, err = AIWorkflowService.PublishWorkflow(request.PublishAIWorkflowRequest{ + WorkflowID: workflow.ID, + Definition: dsl.Definition{ + SchemaVersion: 1, + EntryNodeID: "start_1", + Nodes: []dsl.Node{ + {ID: "start_1", Type: "start"}, + {ID: "create_1", Type: "create_ticket"}, + {ID: "end_1", Type: "end"}, + }, + Edges: []dsl.Edge{ + {ID: "e1", Source: "start_1", Target: "create_1"}, + {ID: "e2", Source: "create_1", Target: "end_1"}, + }, + }, + }, operator) + if err == nil { + t.Fatalf("expected invalid publish to fail") + } + if versions := repositories.AIWorkflowVersionRepository.Find(sqls.DB(), sqls.NewCnd().Eq("workflow_id", workflow.ID)); len(versions) != 0 { + t.Fatalf("expected no versions after invalid publish, got %d", len(versions)) + } +} + func setupAIWorkflowTestDB(t *testing.T) { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})