fix: MCP server ignore_NotFoundError option not working

- Fixed TapByOCR and TapByCV tools to properly handle ignore_NotFoundError option
- Added option parameters to all MCP tool request structures
- Fixed ConvertActionToCallToolRequest methods to extract action options
- Added extractActionOptionsToArguments helper function for consistent option handling
- Extended fix to all MCP tools: SwipeToTapApp, SwipeToTapText, SwipeToTapTexts, TapXY, TapAbsXY
- Added comprehensive tests for option parameter handling
- Updated test expectations to match actual registered tools

This ensures that when ignore_NotFoundError is set to true, OCR/CV operations
will return nil instead of throwing errors when target elements are not found,
allowing tests to continue execution as expected.
This commit is contained in:
lilong.129
2025-05-26 22:02:01 +08:00
parent 9a5e0849de
commit df65f9a828
4 changed files with 375 additions and 46 deletions

View File

@@ -313,12 +313,27 @@ func (t *ToolTapXY) Implement() server.ToolHandlerFunc {
return nil, fmt.Errorf("parse parameters error: %w", err)
}
// Build action options from request structure
var opts []option.ActionOption
// Add boolean options
if tapReq.IgnoreNotFoundError {
opts = append(opts, option.WithIgnoreNotFoundError(true))
}
// Add numeric options
if tapReq.Duration > 0 {
opts = append(opts, option.WithDuration(tapReq.Duration))
}
if tapReq.MaxRetryTimes > 0 {
opts = append(opts, option.WithMaxRetryTimes(tapReq.MaxRetryTimes))
}
// Add default options
opts = append(opts, option.WithPreMarkOperation(true))
// Tap action logic
log.Info().Float64("x", tapReq.X).Float64("y", tapReq.Y).Msg("tapping at coordinates")
opts := []option.ActionOption{
option.WithDuration(tapReq.Duration),
option.WithPreMarkOperation(true),
}
err = driverExt.TapXY(tapReq.X, tapReq.Y, opts...)
if err != nil {
@@ -340,6 +355,10 @@ func (t *ToolTapXY) ConvertActionToCallToolRequest(action MobileAction) (mcp.Cal
if duration := action.ActionOptions.Duration; duration > 0 {
arguments["duration"] = duration
}
// Extract options to arguments
extractActionOptionsToArguments(action.GetOptions(), arguments)
return buildMCPCallToolRequest(t.Name(), arguments), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap params: %v", action.Params)
@@ -372,12 +391,24 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc {
return nil, fmt.Errorf("parse parameters error: %w", err)
}
// Tap absolute XY action logic
log.Info().Float64("x", tapAbsReq.X).Float64("y", tapAbsReq.Y).Msg("tapping at absolute coordinates")
opts := []option.ActionOption{}
// Build action options from request structure
var opts []option.ActionOption
// Add boolean options
if tapAbsReq.IgnoreNotFoundError {
opts = append(opts, option.WithIgnoreNotFoundError(true))
}
// Add numeric options
if tapAbsReq.Duration > 0 {
opts = append(opts, option.WithDuration(tapAbsReq.Duration))
}
if tapAbsReq.MaxRetryTimes > 0 {
opts = append(opts, option.WithMaxRetryTimes(tapAbsReq.MaxRetryTimes))
}
// Tap absolute XY action logic
log.Info().Float64("x", tapAbsReq.X).Float64("y", tapAbsReq.Y).Msg("tapping at absolute coordinates")
err = driverExt.TapAbsXY(tapAbsReq.X, tapAbsReq.Y, opts...)
if err != nil {
@@ -399,12 +430,16 @@ func (t *ToolTapAbsXY) ConvertActionToCallToolRequest(action MobileAction) (mcp.
if duration := action.ActionOptions.Duration; duration > 0 {
arguments["duration"] = duration
}
// Extract options to arguments
extractActionOptionsToArguments(action.GetOptions(), arguments)
return buildMCPCallToolRequest(t.Name(), arguments), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap abs params: %v", action.Params)
}
// ToolTapByOCR implements the tap_by_ocr tool call.
// ToolTapByOCR implements the tap_ocr tool call.
type ToolTapByOCR struct{}
func (t *ToolTapByOCR) Name() option.ActionMethod {
@@ -431,9 +466,31 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc {
return nil, fmt.Errorf("parse parameters error: %w", err)
}
// Build action options from request structure
var opts []option.ActionOption
// Add boolean options
if ocrReq.IgnoreNotFoundError {
opts = append(opts, option.WithIgnoreNotFoundError(true))
}
if ocrReq.Regex {
opts = append(opts, option.WithRegex(true))
}
if ocrReq.TapRandomRect {
opts = append(opts, option.WithTapRandomRect(true))
}
// Add numeric options
if ocrReq.MaxRetryTimes > 0 {
opts = append(opts, option.WithMaxRetryTimes(ocrReq.MaxRetryTimes))
}
if ocrReq.Index > 0 {
opts = append(opts, option.WithIndex(ocrReq.Index))
}
// Tap by OCR action logic
log.Info().Str("text", ocrReq.Text).Msg("tapping by OCR")
err = driverExt.TapByOCR(ocrReq.Text)
err = driverExt.TapByOCR(ocrReq.Text, opts...)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Tap by OCR failed: %s", err.Error())), nil
}
@@ -447,12 +504,16 @@ func (t *ToolTapByOCR) ConvertActionToCallToolRequest(action MobileAction) (mcp.
arguments := map[string]any{
"text": text,
}
// Extract options to arguments
extractActionOptionsToArguments(action.GetOptions(), arguments)
return buildMCPCallToolRequest(t.Name(), arguments), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by OCR params: %v", action.Params)
}
// ToolTapByCV implements the tap_by_cv tool call.
// ToolTapByCV implements the tap_cv tool call.
type ToolTapByCV struct{}
func (t *ToolTapByCV) Name() option.ActionMethod {
@@ -479,13 +540,32 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc {
return nil, fmt.Errorf("parse parameters error: %w", err)
}
// Build action options from request structure
var opts []option.ActionOption
// Add boolean options
if cvReq.IgnoreNotFoundError {
opts = append(opts, option.WithIgnoreNotFoundError(true))
}
if cvReq.TapRandomRect {
opts = append(opts, option.WithTapRandomRect(true))
}
// Add numeric options
if cvReq.MaxRetryTimes > 0 {
opts = append(opts, option.WithMaxRetryTimes(cvReq.MaxRetryTimes))
}
if cvReq.Index > 0 {
opts = append(opts, option.WithIndex(cvReq.Index))
}
// Tap by CV action logic
log.Info().Str("imagePath", cvReq.ImagePath).Msg("tapping by CV")
// For TapByCV, we need to check if there are UI types in the options
// In the original DoAction, it requires ScreenShotWithUITypes to be set
// We'll add a basic implementation that triggers CV recognition
err = driverExt.TapByCV()
err = driverExt.TapByCV(opts...)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Tap by CV failed: %s", err.Error())), nil
}
@@ -499,6 +579,10 @@ func (t *ToolTapByCV) ConvertActionToCallToolRequest(action MobileAction) (mcp.C
arguments := map[string]any{
"imagePath": "", // Will be handled by the tool based on UI types
}
// Extract options to arguments
extractActionOptionsToArguments(action.GetOptions(), arguments)
return buildMCPCallToolRequest(t.Name(), arguments), nil
}
@@ -1002,9 +1086,25 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc {
return nil, fmt.Errorf("parse parameters error: %w", err)
}
// Build action options from request structure
var opts []option.ActionOption
// Add boolean options
if swipeAppReq.IgnoreNotFoundError {
opts = append(opts, option.WithIgnoreNotFoundError(true))
}
// Add numeric options
if swipeAppReq.MaxRetryTimes > 0 {
opts = append(opts, option.WithMaxRetryTimes(swipeAppReq.MaxRetryTimes))
}
if swipeAppReq.Index > 0 {
opts = append(opts, option.WithIndex(swipeAppReq.Index))
}
// Swipe to tap app action logic
log.Info().Str("appName", swipeAppReq.AppName).Msg("swipe to tap app")
err = driverExt.SwipeToTapApp(swipeAppReq.AppName)
err = driverExt.SwipeToTapApp(swipeAppReq.AppName, opts...)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap app failed: %s", err.Error())), nil
}
@@ -1018,6 +1118,10 @@ func (t *ToolSwipeToTapApp) ConvertActionToCallToolRequest(action MobileAction)
arguments := map[string]any{
"appName": appName,
}
// Extract options to arguments
extractActionOptionsToArguments(action.GetOptions(), arguments)
return buildMCPCallToolRequest(t.Name(), arguments), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap app params: %v", action.Params)
@@ -1050,9 +1154,28 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc {
return nil, fmt.Errorf("parse parameters error: %w", err)
}
// Build action options from request structure
var opts []option.ActionOption
// Add boolean options
if swipeTextReq.IgnoreNotFoundError {
opts = append(opts, option.WithIgnoreNotFoundError(true))
}
if swipeTextReq.Regex {
opts = append(opts, option.WithRegex(true))
}
// Add numeric options
if swipeTextReq.MaxRetryTimes > 0 {
opts = append(opts, option.WithMaxRetryTimes(swipeTextReq.MaxRetryTimes))
}
if swipeTextReq.Index > 0 {
opts = append(opts, option.WithIndex(swipeTextReq.Index))
}
// Swipe to tap text action logic
log.Info().Str("text", swipeTextReq.Text).Msg("swipe to tap text")
err = driverExt.SwipeToTapTexts([]string{swipeTextReq.Text})
err = driverExt.SwipeToTapTexts([]string{swipeTextReq.Text}, opts...)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap text failed: %s", err.Error())), nil
}
@@ -1066,6 +1189,10 @@ func (t *ToolSwipeToTapText) ConvertActionToCallToolRequest(action MobileAction)
arguments := map[string]any{
"text": text,
}
// Extract options to arguments
extractActionOptionsToArguments(action.GetOptions(), arguments)
return buildMCPCallToolRequest(t.Name(), arguments), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap text params: %v", action.Params)
@@ -1098,9 +1225,28 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc {
return nil, fmt.Errorf("parse parameters error: %w", err)
}
// Build action options from request structure
var opts []option.ActionOption
// Add boolean options
if swipeTextsReq.IgnoreNotFoundError {
opts = append(opts, option.WithIgnoreNotFoundError(true))
}
if swipeTextsReq.Regex {
opts = append(opts, option.WithRegex(true))
}
// Add numeric options
if swipeTextsReq.MaxRetryTimes > 0 {
opts = append(opts, option.WithMaxRetryTimes(swipeTextsReq.MaxRetryTimes))
}
if swipeTextsReq.Index > 0 {
opts = append(opts, option.WithIndex(swipeTextsReq.Index))
}
// Swipe to tap texts action logic
log.Info().Strs("texts", swipeTextsReq.Texts).Msg("swipe to tap texts")
err = driverExt.SwipeToTapTexts(swipeTextsReq.Texts)
err = driverExt.SwipeToTapTexts(swipeTextsReq.Texts, opts...)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap texts failed: %s", err.Error())), nil
}
@@ -1121,6 +1267,10 @@ func (t *ToolSwipeToTapTexts) ConvertActionToCallToolRequest(action MobileAction
arguments := map[string]any{
"texts": texts,
}
// Extract options to arguments
extractActionOptionsToArguments(action.GetOptions(), arguments)
return buildMCPCallToolRequest(t.Name(), arguments), nil
}
@@ -1198,6 +1348,48 @@ func mapToStruct(m map[string]any, out interface{}) error {
return json.Unmarshal(b, out)
}
// extractActionOptionsToArguments extracts action options and adds them to arguments map
// This is a generic helper that can be used by multiple tools
func extractActionOptionsToArguments(actionOptions []option.ActionOption, arguments map[string]any) {
if len(actionOptions) == 0 {
return
}
// Apply all options to a temporary ActionOptions to extract values
tempOptions := &option.ActionOptions{}
for _, opt := range actionOptions {
opt(tempOptions)
}
// Define option mappings for common boolean options
booleanOptions := map[string]bool{
"ignore_NotFoundError": tempOptions.IgnoreNotFoundError,
"regex": tempOptions.Regex,
"tap_random_rect": tempOptions.TapRandomRect,
}
// Add boolean options only if they are true
for key, value := range booleanOptions {
if value {
arguments[key] = true
}
}
// Add numeric options only if they have meaningful values
if tempOptions.MaxRetryTimes > 0 {
arguments["max_retry_times"] = tempOptions.MaxRetryTimes
}
if tempOptions.Index != 0 {
arguments["index"] = tempOptions.Index
}
if tempOptions.Duration > 0 {
arguments["duration"] = tempOptions.Duration
}
if tempOptions.PressDuration > 0 {
arguments["press_duration"] = tempOptions.PressDuration
}
}
// ToolHome implements the home tool call.
type ToolHome struct{}

View File

@@ -3,6 +3,7 @@ package uixt
import (
"testing"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/stretchr/testify/assert"
)
@@ -18,19 +19,43 @@ func TestNewMCPServer(t *testing.T) {
expectedTools := []string{
"list_available_devices",
"select_device",
"tap_xy",
"tap_abs_xy",
"tap_ocr",
"tap_cv",
"double_tap_xy",
"swipe_direction",
"swipe_coordinate",
"swipe_to_tap_app",
"swipe_to_tap_text",
"swipe_to_tap_texts",
"drag",
"input",
"screenshot",
"get_screen_size",
"press_button",
"home",
"back",
"list_packages",
"app_launch",
"app_terminate",
"get_screen_size",
"press_button",
"tap_xy",
"swipe",
"drag",
"screenshot",
"home",
"back",
"input",
"app_install",
"app_uninstall",
"app_clear",
"sleep",
"sleep_ms",
"sleep_random",
"set_ime",
"get_source",
"close_popups",
"web_login_none_ui",
"secondary_click",
"hover_by_selector",
"tap_by_selector",
"secondary_click_by_selector",
"web_close_tab",
"ai_action",
"finished",
}
registeredToolNames := make(map[string]bool)
@@ -48,19 +73,43 @@ func TestToolInterfaces(t *testing.T) {
tools := []ActionTool{
&ToolListAvailableDevices{},
&ToolSelectDevice{},
&ToolTapXY{},
&ToolTapAbsXY{},
&ToolTapByOCR{},
&ToolTapByCV{},
&ToolDoubleTapXY{},
&ToolSwipeDirection{},
&ToolSwipeCoordinate{},
&ToolSwipeToTapApp{},
&ToolSwipeToTapText{},
&ToolSwipeToTapTexts{},
&ToolDrag{},
&ToolInput{},
&ToolScreenShot{},
&ToolGetScreenSize{},
&ToolPressButton{},
&ToolHome{},
&ToolBack{},
&ToolListPackages{},
&ToolLaunchApp{},
&ToolTerminateApp{},
&ToolGetScreenSize{},
&ToolPressButton{},
&ToolTapXY{},
&ToolSwipeDirection{},
&ToolDrag{},
&ToolScreenShot{},
&ToolHome{},
&ToolBack{},
&ToolInput{},
&ToolAppInstall{},
&ToolAppUninstall{},
&ToolAppClear{},
&ToolSleep{},
&ToolSleepMS{},
&ToolSleepRandom{},
&ToolSetIme{},
&ToolGetSource{},
&ToolClosePopups{},
&ToolWebLoginNoneUI{},
&ToolSecondaryClick{},
&ToolHoverBySelector{},
&ToolTapBySelector{},
&ToolSecondaryClickBySelector{},
&ToolWebCloseTab{},
&ToolAIAction{},
&ToolFinished{},
}
for _, tool := range tools {
@@ -70,3 +119,65 @@ func TestToolInterfaces(t *testing.T) {
assert.NotNil(t, tool.Implement(), "Tool implementation should not be nil")
}
}
func TestIgnoreNotFoundErrorOption(t *testing.T) {
// Test that ignore_NotFoundError option is properly extracted and applied
server := NewMCPServer()
// Test TapByOCR tool
tapOCRTool := server.GetToolByAction(option.ACTION_TapByOCR)
assert.NotNil(t, tapOCRTool, "TapByOCR tool should be available")
// Create a mock action with ignore_NotFoundError option
actionOptions := option.NewActionOptions(
option.WithIgnoreNotFoundError(true),
option.WithMaxRetryTimes(2),
option.WithIndex(1),
option.WithRegex(true),
option.WithTapRandomRect(true),
)
action := MobileAction{
Method: option.ACTION_TapByOCR,
Params: "test_text",
ActionOptions: *actionOptions,
}
// Convert action to MCP call tool request
request, err := tapOCRTool.ConvertActionToCallToolRequest(action)
assert.NoError(t, err, "Should convert action to request without error")
// Verify that ignore_NotFoundError option is included in arguments
args := request.Params.Arguments
assert.Equal(t, true, args["ignore_NotFoundError"], "ignore_NotFoundError should be true")
assert.Equal(t, 2, args["max_retry_times"], "max_retry_times should be 2")
assert.Equal(t, 1, args["index"], "index should be 1")
assert.Equal(t, true, args["regex"], "regex should be true")
assert.Equal(t, true, args["tap_random_rect"], "tap_random_rect should be true")
assert.Equal(t, "test_text", args["text"], "text should be test_text")
}
func TestExtractActionOptionsToArguments(t *testing.T) {
// Test the extractActionOptionsToArguments helper function
actionOptions := []option.ActionOption{
option.WithIgnoreNotFoundError(true),
option.WithMaxRetryTimes(3),
option.WithIndex(2),
option.WithRegex(true),
option.WithTapRandomRect(false), // false should not be included
option.WithDuration(1.5),
}
arguments := make(map[string]any)
extractActionOptionsToArguments(actionOptions, arguments)
// Verify extracted options
assert.Equal(t, true, arguments["ignore_NotFoundError"], "ignore_NotFoundError should be extracted")
assert.Equal(t, 3, arguments["max_retry_times"], "max_retry_times should be extracted")
assert.Equal(t, 2, arguments["index"], "index should be extracted")
assert.Equal(t, true, arguments["regex"], "regex should be extracted")
assert.Equal(t, 1.5, arguments["duration"], "duration should be extracted")
// tap_random_rect should not be included since it's false
_, exists := arguments["tap_random_rect"]
assert.False(t, exists, "tap_random_rect should not be included when false")
}

View File

@@ -16,9 +16,11 @@ type TargetDeviceRequest struct {
type TapRequest struct {
TargetDeviceRequest
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for the tap action"`
}
type DragRequest struct {
@@ -103,17 +105,28 @@ type WebLoginNoneUIRequest struct {
type SwipeToTapAppRequest struct {
TargetDeviceRequest
AppName string `json:"appName" binding:"required" desc:"App name to find and tap"`
AppName string `json:"appName" binding:"required" desc:"App name to find and tap"`
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the app"`
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
}
type SwipeToTapTextRequest struct {
TargetDeviceRequest
Text string `json:"text" binding:"required" desc:"Text to find and tap"`
Text string `json:"text" binding:"required" desc:"Text to find and tap"`
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the text"`
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
Regex bool `json:"regex" desc:"Use regex to match text"`
}
type SwipeToTapTextsRequest struct {
TargetDeviceRequest
Texts []string `json:"texts" binding:"required" desc:"List of texts to find and tap"`
Texts []string `json:"texts" binding:"required" desc:"List of texts to find and tap"`
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the texts"`
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
Regex bool `json:"regex" desc:"Use regex to match text"`
}
type SecondaryClickRequest struct {
@@ -144,25 +157,38 @@ type GetSourceRequest struct {
type TapAbsXYRequest struct {
TargetDeviceRequest
X float64 `json:"x" binding:"required" desc:"Absolute X coordinate in pixels"`
Y float64 `json:"y" binding:"required" desc:"Absolute Y coordinate in pixels"`
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
X float64 `json:"x" binding:"required" desc:"Absolute X coordinate in pixels"`
Y float64 `json:"y" binding:"required" desc:"Absolute Y coordinate in pixels"`
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for the tap action"`
}
type TapByOCRRequest struct {
TargetDeviceRequest
Text string `json:"text" binding:"required" desc:"OCR text to find and tap"`
Text string `json:"text" binding:"required" desc:"OCR text to find and tap"`
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the text"`
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
Regex bool `json:"regex" desc:"Use regex to match text"`
TapRandomRect bool `json:"tap_random_rect" desc:"Tap random point in text rectangle"`
}
type TapByCVRequest struct {
TargetDeviceRequest
ImagePath string `json:"imagePath" desc:"Path to reference image for CV recognition"`
ImagePath string `json:"imagePath" desc:"Path to reference image for CV recognition"`
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the image"`
Index int `json:"index" desc:"Index of the target element when multiple matches found"`
TapRandomRect bool `json:"tap_random_rect" desc:"Tap random point in image rectangle"`
}
type DoubleTapXYRequest struct {
TargetDeviceRequest
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"`
MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for the tap action"`
}
type SwipeAdvancedRequest struct {