mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 07:26:55 +08:00
refactor: unify action execution interface and merge AI action handling
This commit is contained in:
+87
-8
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/json"
|
||||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
@@ -88,37 +90,114 @@ func (c *MCPClient4XTDriver) GetToolByAction(actionName option.ActionName) Actio
|
||||
return c.Server.GetToolByAction(actionName)
|
||||
}
|
||||
|
||||
func (dExt *XTDriver) ExecuteAction(ctx context.Context, action option.MobileAction) (err error) {
|
||||
func (dExt *XTDriver) ExecuteAction(ctx context.Context, action option.MobileAction) ([]*SubActionResult, error) {
|
||||
subActionStartTime := time.Now()
|
||||
|
||||
// Find the corresponding tool for this action method
|
||||
tool := dExt.client.Server.GetToolByAction(action.Method)
|
||||
if tool == nil {
|
||||
return fmt.Errorf("no tool found for action method: %s", action.Method)
|
||||
return nil, fmt.Errorf("no tool found for action method: %s", action.Method)
|
||||
}
|
||||
|
||||
// Use the tool's own conversion method
|
||||
req, err := tool.ConvertActionToCallToolRequest(action)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert action to MCP tool call: %w", err)
|
||||
return nil, fmt.Errorf("failed to convert action to MCP tool call: %w", err)
|
||||
}
|
||||
|
||||
// Create sub-action result
|
||||
subActionResult := &SubActionResult{
|
||||
ActionName: string(action.Method),
|
||||
Arguments: action.Params,
|
||||
StartTime: subActionStartTime.Unix(),
|
||||
}
|
||||
|
||||
// Execute via MCP tool
|
||||
result, err := dExt.client.CallTool(ctx, req)
|
||||
subActionResult.Elapsed = time.Since(subActionStartTime).Milliseconds()
|
||||
if err != nil {
|
||||
return fmt.Errorf("MCP tool call failed: %w", err)
|
||||
subActionResult.Error = err
|
||||
return []*SubActionResult{subActionResult}, fmt.Errorf("MCP tool call failed: %w", err)
|
||||
}
|
||||
|
||||
// Check if the tool execution had business logic errors
|
||||
if result.IsError {
|
||||
var errMsg string
|
||||
if len(result.Content) > 0 {
|
||||
return fmt.Errorf("invoke tool %s failed: %v",
|
||||
tool.Name(), result.Content)
|
||||
errMsg = fmt.Sprintf("invoke tool %s failed: %v", tool.Name(), result.Content)
|
||||
} else {
|
||||
errMsg = fmt.Sprintf("invoke tool %s failed", tool.Name())
|
||||
}
|
||||
return fmt.Errorf("invoke tool %s failed", tool.Name())
|
||||
err := errors.New(errMsg)
|
||||
subActionResult.Error = err
|
||||
return []*SubActionResult{subActionResult}, err
|
||||
}
|
||||
|
||||
// Handle special AI actions (start_to_goal, ai_action) that return sub-actions
|
||||
if action.Method == option.ACTION_StartToGoal || action.Method == option.ACTION_AIAction {
|
||||
return dExt.parseAIActionResult(result, subActionResult)
|
||||
}
|
||||
|
||||
// For regular actions, collect session data and return single sub-action result
|
||||
subActionData := dExt.GetData(true) // reset after getting data
|
||||
|
||||
// Add requests if any
|
||||
if requests, ok := subActionData["requests"].([]*DriverRequests); ok && len(requests) > 0 {
|
||||
subActionResult.Requests = requests
|
||||
}
|
||||
|
||||
// Add screen_results if any
|
||||
if screenResults, ok := subActionData["screen_results"].([]*ScreenResult); ok && len(screenResults) > 0 {
|
||||
subActionResult.ScreenResults = screenResults
|
||||
}
|
||||
|
||||
log.Debug().Str("tool", string(tool.Name())).
|
||||
Msg("execute action via MCP tool")
|
||||
return nil
|
||||
return []*SubActionResult{subActionResult}, nil
|
||||
}
|
||||
|
||||
// parseAIActionResult parses the result from AI actions (start_to_goal, ai_action) and extracts sub-actions
|
||||
func (dExt *XTDriver) parseAIActionResult(result *mcp.CallToolResult, originalSubAction *SubActionResult) ([]*SubActionResult, error) {
|
||||
// Parse the JSON response to extract sub_actions
|
||||
var responseData map[string]interface{}
|
||||
if len(result.Content) > 0 {
|
||||
// Get the first text content
|
||||
if textContent, ok := result.Content[0].(mcp.TextContent); ok {
|
||||
if err := json.Unmarshal([]byte(textContent.Text), &responseData); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to parse AI action result, falling back to single action")
|
||||
return []*SubActionResult{originalSubAction}, nil
|
||||
}
|
||||
} else {
|
||||
log.Warn().Msg("AI action result is not text content, falling back to single action")
|
||||
return []*SubActionResult{originalSubAction}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Extract sub_actions from the response
|
||||
if subActionsData, ok := responseData["sub_actions"]; ok {
|
||||
// Convert to JSON and back to properly deserialize SubActionResult structs
|
||||
subActionsJSON, err := json.Marshal(subActionsData)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to marshal sub_actions, falling back to single action")
|
||||
return []*SubActionResult{originalSubAction}, nil
|
||||
}
|
||||
|
||||
var subActionResults []*SubActionResult
|
||||
if err := json.Unmarshal(subActionsJSON, &subActionResults); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to unmarshal sub_actions, falling back to single action")
|
||||
return []*SubActionResult{originalSubAction}, nil
|
||||
}
|
||||
|
||||
log.Debug().Int("sub_actions_count", len(subActionResults)).
|
||||
Str("action", string(originalSubAction.ActionName)).
|
||||
Msg("parsed AI action sub-actions")
|
||||
return subActionResults, nil
|
||||
}
|
||||
|
||||
// If no sub_actions found, return the original action as a single result
|
||||
log.Debug().Str("action", string(originalSubAction.ActionName)).
|
||||
Msg("no sub_actions found in AI action result, using single action")
|
||||
return []*SubActionResult{originalSubAction}, nil
|
||||
}
|
||||
|
||||
// NewDeviceWithDefault is a helper function to create a device with default options
|
||||
|
||||
Reference in New Issue
Block a user