- Updated execution control methods (`PauseExecution`, `ResumeExecution`, `StopExecution`) to include error handling and database status updates for paused, running, and cancelled states. - Introduced `ExecuteWithControl` method in the Executor interface to manage execution with pause/resume capabilities, allowing for better control during execution. - Enhanced the `Execute` methods across different executors (DryRun, Sandbox, Standard) to support pre-generated execution IDs and control mechanisms. - Improved error messages in the execution handling API to provide clearer feedback on execution status and errors. - Added support for a new execution status `ExecPaused` in the execution model, enhancing the tracking of execution states.
349 lines
10 KiB
Go
349 lines
10 KiB
Go
package robot
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yaoapp/kun/log"
|
|
robotapi "github.com/yaoapp/yao/agent/robot/api"
|
|
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
|
"github.com/yaoapp/yao/openapi/response"
|
|
)
|
|
|
|
// ==================== Execution Handlers ====================
|
|
// Permission Note: Execution permissions are inherited from the parent robot.
|
|
// Check robot's __yao_team_id and __yao_created_by for access control.
|
|
|
|
// ListExecutions lists executions for a robot
|
|
// GET /v1/agent/robots/:id/executions
|
|
func ListExecutions(c *gin.Context) {
|
|
// Get authorized information
|
|
authInfo := authorized.GetInfo(c)
|
|
|
|
// Get robot ID from URL parameter
|
|
robotID := c.Param("id")
|
|
if robotID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "robot id is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Create robot context
|
|
ctx := &robottypes.Context{}
|
|
|
|
// Check robot permission first (executions inherit robot permission)
|
|
robotResp, err := robotapi.GetRobotResponse(ctx, robotID)
|
|
if err != nil {
|
|
if errors.Is(err, robottypes.ErrRobotNotFound) {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Robot not found: " + robotID,
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to get robot: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Check read permission on robot
|
|
if !CanRead(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: "Forbidden: No permission to access this robot's executions",
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
return
|
|
}
|
|
|
|
// Parse query parameters
|
|
var filter ExecutionFilter
|
|
if err := c.ShouldBindQuery(&filter); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid query parameters: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Apply defaults
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.PageSize > 100 {
|
|
filter.PageSize = 100
|
|
}
|
|
|
|
// Build API query
|
|
query := &robotapi.ExecutionQuery{
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
}
|
|
if filter.Status != "" {
|
|
query.Status = robottypes.ExecStatus(filter.Status)
|
|
}
|
|
if filter.TriggerType != "" {
|
|
query.Trigger = robottypes.TriggerType(filter.TriggerType)
|
|
}
|
|
|
|
// Call API layer
|
|
result, err := robotapi.ListExecutions(ctx, robotID, query)
|
|
if err != nil {
|
|
log.Error("Failed to list executions for robot %s: %v", robotID, err)
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to list executions: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Convert to response (brief format for list)
|
|
data := make([]*ExecutionResponse, 0, len(result.Data))
|
|
for _, exec := range result.Data {
|
|
data = append(data, NewExecutionResponseBrief(exec))
|
|
}
|
|
|
|
resp := &ExecutionListResponse{
|
|
Data: data,
|
|
Total: result.Total,
|
|
Page: result.Page,
|
|
PageSize: result.PageSize,
|
|
}
|
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
|
}
|
|
|
|
// GetExecution gets a single execution by ID
|
|
// GET /v1/agent/robots/:id/executions/:exec_id
|
|
func GetExecution(c *gin.Context) {
|
|
// Get authorized information
|
|
authInfo := authorized.GetInfo(c)
|
|
|
|
// Get robot ID and execution ID from URL parameters
|
|
robotID := c.Param("id")
|
|
execID := c.Param("exec_id")
|
|
|
|
if robotID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "robot id is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
if execID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "execution id is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Create robot context
|
|
ctx := &robottypes.Context{}
|
|
|
|
// Check robot permission first
|
|
robotResp, err := robotapi.GetRobotResponse(ctx, robotID)
|
|
if err != nil {
|
|
if errors.Is(err, robottypes.ErrRobotNotFound) {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Robot not found: " + robotID,
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to get robot: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Check read permission on robot
|
|
if !CanRead(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: "Forbidden: No permission to access this robot's executions",
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
return
|
|
}
|
|
|
|
// Get execution
|
|
exec, err := robotapi.GetExecution(ctx, execID)
|
|
if err != nil {
|
|
log.Error("Failed to get execution %s: %v", execID, err)
|
|
|
|
if err.Error() == "execution not found: "+execID {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Execution not found: " + execID,
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to get execution: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Verify execution belongs to this robot
|
|
if exec.MemberID != robotID {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Execution does not belong to this robot",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
|
|
// Convert to response (full format for detail)
|
|
resp := NewExecutionResponseFromExecution(exec)
|
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
|
}
|
|
|
|
// PauseExecution pauses a running execution
|
|
// POST /v1/agent/robots/:id/executions/:exec_id/pause
|
|
func PauseExecution(c *gin.Context) {
|
|
handleExecutionControl(c, "pause")
|
|
}
|
|
|
|
// ResumeExecution resumes a paused execution
|
|
// POST /v1/agent/robots/:id/executions/:exec_id/resume
|
|
func ResumeExecution(c *gin.Context) {
|
|
handleExecutionControl(c, "resume")
|
|
}
|
|
|
|
// CancelExecution cancels/stops an execution
|
|
// POST /v1/agent/robots/:id/executions/:exec_id/cancel
|
|
func CancelExecution(c *gin.Context) {
|
|
handleExecutionControl(c, "cancel")
|
|
}
|
|
|
|
// handleExecutionControl handles pause/resume/cancel operations
|
|
func handleExecutionControl(c *gin.Context, action string) {
|
|
// Get authorized information
|
|
authInfo := authorized.GetInfo(c)
|
|
|
|
// Get robot ID and execution ID from URL parameters
|
|
robotID := c.Param("id")
|
|
execID := c.Param("exec_id")
|
|
|
|
if robotID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "robot id is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
if execID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "execution id is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Create robot context
|
|
ctx := &robottypes.Context{}
|
|
|
|
// Check robot permission first
|
|
robotResp, err := robotapi.GetRobotResponse(ctx, robotID)
|
|
if err != nil {
|
|
if errors.Is(err, robottypes.ErrRobotNotFound) {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Robot not found: " + robotID,
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to get robot: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Check write permission on robot (control operations require write)
|
|
if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: "Forbidden: No permission to control this robot's executions",
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
return
|
|
}
|
|
|
|
// Execute the control action
|
|
var controlErr error
|
|
switch action {
|
|
case "pause":
|
|
controlErr = robotapi.PauseExecution(ctx, execID)
|
|
case "resume":
|
|
controlErr = robotapi.ResumeExecution(ctx, execID)
|
|
case "cancel":
|
|
controlErr = robotapi.StopExecution(ctx, execID)
|
|
default:
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid action: " + action,
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
if controlErr != nil {
|
|
log.Error("Failed to %s execution %s: %v", action, execID, controlErr)
|
|
|
|
// Check for common errors
|
|
errMsg := controlErr.Error()
|
|
if errMsg == "execution_id is required" || strings.Contains(errMsg, "execution not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Execution not found or not running: " + execID,
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
return
|
|
}
|
|
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to " + action + " execution: " + controlErr.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Return success
|
|
resp := &ExecutionControlResponse{
|
|
ExecutionID: execID,
|
|
Action: action + "d", // paused, resumed, cancelled
|
|
Success: true,
|
|
Message: "Execution " + action + "d successfully",
|
|
}
|
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
|
}
|