- Added `Name` and `CurrentTaskName` fields to the `Execution` struct for improved UI display during execution phases. - Implemented localization support for UI messages, allowing dynamic updates based on the execution context and user locale. - Updated the executor to manage these fields at various phases, ensuring accurate representation of execution status. - Enhanced OpenAPI documentation to reflect the new fields and their usage in execution responses. - Added unit tests to validate the functionality of UI fields and localization handling.
271 lines
7.8 KiB
Go
271 lines
7.8 KiB
Go
package robot
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yaoapp/kun/log"
|
|
agentcontext "github.com/yaoapp/yao/agent/context"
|
|
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"
|
|
)
|
|
|
|
// ==================== Trigger Handlers ====================
|
|
// Permission Note: Same as execution - check robot's permission.
|
|
|
|
// TriggerRobot triggers a robot execution
|
|
// POST /v1/agent/robots/:id/trigger
|
|
func TriggerRobot(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
|
|
}
|
|
|
|
// Parse request body
|
|
var req TriggerRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
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 (trigger requires write permission)
|
|
if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: "Forbidden: No permission to trigger this robot",
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
return
|
|
}
|
|
|
|
// Build API trigger request
|
|
apiReq := buildAPITriggerRequest(&req)
|
|
|
|
// Call API layer
|
|
result, err := robotapi.Trigger(ctx, robotID, apiReq)
|
|
if err != nil {
|
|
log.Error("Failed to trigger robot %s: %v", robotID, err)
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to trigger robot: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Convert to response
|
|
resp := &TriggerResponse{
|
|
Accepted: result.Accepted,
|
|
ExecutionID: result.ExecutionID,
|
|
Queued: result.Queued,
|
|
Message: result.Message,
|
|
}
|
|
|
|
if result.Accepted {
|
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
|
} else {
|
|
// Trigger was not accepted (e.g., queue full, robot paused)
|
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
|
}
|
|
}
|
|
|
|
// InterveneRobot performs human intervention on a robot
|
|
// POST /v1/agent/robots/:id/intervene
|
|
func InterveneRobot(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
|
|
}
|
|
|
|
// Parse request body
|
|
var req InterveneRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Validate action
|
|
if req.Action == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "action 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 (intervention requires write permission)
|
|
if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: "Forbidden: No permission to intervene with this robot",
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
return
|
|
}
|
|
|
|
// Build API trigger request for intervention
|
|
apiReq := &robotapi.TriggerRequest{
|
|
Type: robottypes.TriggerHuman,
|
|
Action: robottypes.InterventionAction(req.Action),
|
|
PlanAt: req.PlanAt,
|
|
}
|
|
|
|
// Convert messages
|
|
if len(req.Messages) > 0 {
|
|
apiReq.Messages = convertMessagesToContext(req.Messages)
|
|
}
|
|
|
|
// Call API layer (Intervene uses TriggerHuman internally)
|
|
result, err := robotapi.Intervene(ctx, robotID, apiReq)
|
|
if err != nil {
|
|
log.Error("Failed to intervene with robot %s: %v", robotID, err)
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to intervene: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
return
|
|
}
|
|
|
|
// Convert to response
|
|
resp := &InterveneResponse{
|
|
Accepted: result.Accepted,
|
|
ExecutionID: result.ExecutionID,
|
|
Message: result.Message,
|
|
}
|
|
|
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
|
}
|
|
|
|
// ==================== Helper Functions ====================
|
|
|
|
// buildAPITriggerRequest builds robotapi.TriggerRequest from HTTP request
|
|
func buildAPITriggerRequest(req *TriggerRequest) *robotapi.TriggerRequest {
|
|
apiReq := &robotapi.TriggerRequest{}
|
|
|
|
// Set trigger type (default to human)
|
|
switch req.TriggerType {
|
|
case "event":
|
|
apiReq.Type = robottypes.TriggerEvent
|
|
case "clock":
|
|
apiReq.Type = robottypes.TriggerClock
|
|
default:
|
|
apiReq.Type = robottypes.TriggerHuman
|
|
}
|
|
|
|
// Human intervention fields
|
|
if req.Action != "" {
|
|
apiReq.Action = robottypes.InterventionAction(req.Action)
|
|
}
|
|
if len(req.Messages) > 0 {
|
|
apiReq.Messages = convertMessagesToContext(req.Messages)
|
|
}
|
|
|
|
// Event fields
|
|
if req.Source != "" {
|
|
apiReq.Source = robottypes.EventSource(req.Source)
|
|
}
|
|
if req.EventType != "" {
|
|
apiReq.EventType = req.EventType
|
|
}
|
|
if req.Data != nil {
|
|
apiReq.Data = req.Data
|
|
}
|
|
|
|
// Executor mode
|
|
if req.ExecutorMode != "" {
|
|
apiReq.ExecutorMode = robottypes.ExecutorMode(req.ExecutorMode)
|
|
}
|
|
|
|
// i18n locale
|
|
if req.Locale != "" {
|
|
apiReq.Locale = req.Locale
|
|
}
|
|
|
|
return apiReq
|
|
}
|
|
|
|
// convertMessagesToContext converts MessageItem slice to agent context messages
|
|
func convertMessagesToContext(msgs []MessageItem) []agentcontext.Message {
|
|
result := make([]agentcontext.Message, 0, len(msgs))
|
|
for _, m := range msgs {
|
|
result = append(result, agentcontext.Message{
|
|
Role: agentcontext.MessageRole(m.Role),
|
|
Content: m.Content,
|
|
})
|
|
}
|
|
return result
|
|
}
|