diff --git a/internal/version/VERSION b/internal/version/VERSION index 99c3dee5..12710754 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505262125 +v5.0.0-beta-2505262202 diff --git a/uixt/mcp_server.go b/uixt/mcp_server.go index e338a885..4f07db20 100644 --- a/uixt/mcp_server.go +++ b/uixt/mcp_server.go @@ -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{} diff --git a/uixt/mcp_server_test.go b/uixt/mcp_server_test.go index 21d04c35..3d7f5064 100644 --- a/uixt/mcp_server_test.go +++ b/uixt/mcp_server_test.go @@ -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") +} diff --git a/uixt/option/request.go b/uixt/option/request.go index 11722328..7fa51989 100644 --- a/uixt/option/request.go +++ b/uixt/option/request.go @@ -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 {