diff --git a/examples/uitest/demo_android_feed_swipe.json b/examples/uitest/demo_android_feed_swipe.json index de33d6f4..9a0f8cd0 100644 --- a/examples/uitest/demo_android_feed_swipe.json +++ b/examples/uitest/demo_android_feed_swipe.json @@ -7,7 +7,7 @@ "android": [ { "serial": "$device", - "log_on": true, + "log_on": false, "adb_server_host": "localhost", "adb_server_port": 5037, "uia2_ip": "localhost", diff --git a/internal/version/VERSION b/internal/version/VERSION index e3542c4b..cf59a272 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505271149 +v5.0.0-beta-2505271334 diff --git a/server/app.go b/server/app.go index 951e26db..57a5f07b 100644 --- a/server/app.go +++ b/server/app.go @@ -22,7 +22,7 @@ func (r *Router) foregroundAppHandler(c *gin.Context) { } func (r *Router) appInfoHandler(c *gin.Context) { - var req option.UnifiedActionRequest + var req option.ActionOptions if err := c.ShouldBindQuery(&req); err != nil { RenderErrorValidateRequest(c, err) return diff --git a/server/key.go b/server/key.go index bf585cdc..8f1e192b 100644 --- a/server/key.go +++ b/server/key.go @@ -39,7 +39,7 @@ func (r *Router) backspaceHandler(c *gin.Context) { return } - count := req.GetCount() + count := req.Count if count == 0 { count = 20 } @@ -67,7 +67,7 @@ func (r *Router) keycodeHandler(c *gin.Context) { } // TODO FIXME err = driver.IDriver.(*uixt.ADBDriver). - PressKeyCode(uixt.KeyCode(req.GetKeycode()), uixt.KMEmpty) + PressKeyCode(uixt.KeyCode(req.Keycode), uixt.KMEmpty) if err != nil { RenderError(c, err) return diff --git a/server/ui.go b/server/ui.go index b3f1d363..8d9e1e0a 100644 --- a/server/ui.go +++ b/server/ui.go @@ -7,8 +7,8 @@ import ( ) // processUnifiedRequest is a helper function to handle common request processing -func (r *Router) processUnifiedRequest(c *gin.Context, actionType option.ActionMethod) (*option.UnifiedActionRequest, error) { - var req option.UnifiedActionRequest +func (r *Router) processUnifiedRequest(c *gin.Context, actionType option.ActionName) (*option.ActionOptions, error) { + var req option.ActionOptions // Bind JSON request if err := c.ShouldBindJSON(&req); err != nil { @@ -29,7 +29,7 @@ func (r *Router) processUnifiedRequest(c *gin.Context, actionType option.ActionM } // setRequestContextFromURL sets platform and serial from URL parameters -func setRequestContextFromURL(c *gin.Context, req *option.UnifiedActionRequest) { +func setRequestContextFromURL(c *gin.Context, req *option.ActionOptions) { if req.Platform == "" { req.Platform = c.Param("platform") } @@ -49,12 +49,11 @@ func (r *Router) tapHandler(c *gin.Context) { return } - // Use UnifiedActionRequest directly - if req.GetDuration() > 0 { - err = driver.Drag(req.GetX(), req.GetY(), req.GetX(), req.GetY(), - option.WithDuration(req.GetDuration())) + if req.Duration > 0 { + err = driver.Drag(req.X, req.Y, req.X, req.Y, + option.WithDuration(req.Duration)) } else { - err = driver.TapXY(req.GetX(), req.GetY()) + err = driver.TapXY(req.X, req.Y) } if err != nil { RenderError(c, err) @@ -74,7 +73,7 @@ func (r *Router) rightClickHandler(c *gin.Context) { return } err = driver.IDriver.(*uixt.BrowserDriver). - SecondaryClick(req.GetX(), req.GetY()) + SecondaryClick(req.X, req.Y) if err != nil { RenderError(c, err) return @@ -117,7 +116,7 @@ func (r *Router) hoverHandler(c *gin.Context) { } err = driver.IDriver.(*uixt.BrowserDriver). - Hover(req.GetX(), req.GetY()) + Hover(req.X, req.Y) if err != nil { RenderError(c, err) @@ -139,7 +138,7 @@ func (r *Router) scrollHandler(c *gin.Context) { } err = driver.IDriver.(*uixt.BrowserDriver). - Scroll(req.GetDelta()) + Scroll(req.Delta) if err != nil { RenderError(c, err) @@ -159,7 +158,7 @@ func (r *Router) doubleTapHandler(c *gin.Context) { return } - err = driver.DoubleTap(req.GetX(), req.GetY()) + err = driver.DoubleTap(req.X, req.Y) if err != nil { RenderError(c, err) return @@ -173,7 +172,7 @@ func (r *Router) dragHandler(c *gin.Context) { return } - duration := req.GetDuration() + duration := req.Duration if duration == 0 { duration = 1 } @@ -182,9 +181,9 @@ func (r *Router) dragHandler(c *gin.Context) { return } - err = driver.Drag(req.GetFromX(), req.GetFromY(), req.GetToX(), req.GetToY(), + err = driver.Drag(req.FromX, req.FromY, req.ToX, req.ToY, option.WithDuration(duration), - option.WithPressDuration(req.GetPressDuration())) + option.WithPressDuration(req.PressDuration)) if err != nil { RenderError(c, err) return @@ -202,7 +201,7 @@ func (r *Router) inputHandler(c *gin.Context) { if err != nil { return } - err = driver.Input(req.Text, option.WithFrequency(req.GetFrequency())) + err = driver.Input(req.Text, option.WithFrequency(req.Frequency)) if err != nil { RenderError(c, err) return diff --git a/server/ui_test.go b/server/ui_test.go index 146d984d..6d2430bb 100644 --- a/server/ui_test.go +++ b/server/ui_test.go @@ -18,17 +18,17 @@ func TestTapHandler(t *testing.T) { tests := []struct { name string path string - req option.UnifiedActionRequest + req option.ActionOptions wantStatus int wantResp HttpResponse }{ { name: "tap abs xy", path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), - req: option.UnifiedActionRequest{ - X: &[]float64{500}[0], - Y: &[]float64{800}[0], - Duration: &[]float64{0}[0], + req: option.ActionOptions{ + X: 500.0, + Y: 800.0, + Duration: 0, }, wantStatus: http.StatusOK, wantResp: HttpResponse{ @@ -40,10 +40,10 @@ func TestTapHandler(t *testing.T) { { name: "tap relative xy", path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), - req: option.UnifiedActionRequest{ - X: &[]float64{0.5}[0], - Y: &[]float64{0.6}[0], - Duration: &[]float64{0}[0], + req: option.ActionOptions{ + X: 0.5, + Y: 0.6, + Duration: 0, }, wantStatus: http.StatusOK, wantResp: HttpResponse{ diff --git a/step_ui.go b/step_ui.go index 3cd85946..3b776ef6 100644 --- a/step_ui.go +++ b/step_ui.go @@ -67,7 +67,7 @@ func (s *StepMobile) Serial(serial string) *StepMobile { return s } -func (s *StepMobile) Log(actionName option.ActionMethod) *StepMobile { +func (s *StepMobile) Log(actionName option.ActionName) *StepMobile { s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{ Method: option.ACTION_LOG, Params: actionName, @@ -798,7 +798,7 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err // stat uixt action if action.Method == option.ACTION_LOG { log.Info().Interface("action", action.Params).Msg("stat uixt action") - actionMethod := option.ActionMethod(action.Params.(string)) + actionMethod := option.ActionName(action.Params.(string)) s.summary.Stat.Actions[actionMethod]++ continue } diff --git a/summary.go b/summary.go index d1dc1244..821f67f9 100644 --- a/summary.go +++ b/summary.go @@ -28,7 +28,7 @@ func NewSummary() *Summary { Success: true, Stat: &Stat{ TestSteps: TestStepStat{ - Actions: make(map[option.ActionMethod]int), + Actions: make(map[option.ActionName]int), }, }, Time: &TestCaseTime{ @@ -146,10 +146,10 @@ type TestCaseStat struct { } type TestStepStat struct { - Total int `json:"total" yaml:"total"` - Successes int `json:"successes" yaml:"successes"` - Failures int `json:"failures" yaml:"failures"` - Actions map[option.ActionMethod]int `json:"actions" yaml:"actions"` // record action stats + Total int `json:"total" yaml:"total"` + Successes int `json:"successes" yaml:"successes"` + Failures int `json:"failures" yaml:"failures"` + Actions map[option.ActionName]int `json:"actions" yaml:"actions"` // record action stats } type TestCaseTime struct { @@ -167,7 +167,7 @@ func NewCaseSummary() *TestCaseSummary { return &TestCaseSummary{ Success: true, Stat: &TestStepStat{ - Actions: make(map[option.ActionMethod]int), + Actions: make(map[option.ActionName]int), }, Time: &TestCaseTime{ StartAt: time.Now(), diff --git a/uixt/driver_action.go b/uixt/driver_action.go index 84b3510a..426f7b77 100644 --- a/uixt/driver_action.go +++ b/uixt/driver_action.go @@ -5,7 +5,7 @@ import ( ) type MobileAction struct { - Method option.ActionMethod `json:"method,omitempty" yaml:"method,omitempty"` + Method option.ActionName `json:"method,omitempty" yaml:"method,omitempty"` Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` Options *option.ActionOptions `json:"options,omitempty" yaml:"options,omitempty"` option.ActionOptions diff --git a/uixt/driver_ext_screenshot.go b/uixt/driver_ext_screenshot.go index 2f68fab9..0ea0e7ae 100644 --- a/uixt/driver_ext_screenshot.go +++ b/uixt/driver_ext_screenshot.go @@ -322,7 +322,7 @@ func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error } // MarkUIOperation add operation mark for UI operation -func MarkUIOperation(driver IDriver, actionType option.ActionMethod, actionCoordinates []float64) error { +func MarkUIOperation(driver IDriver, actionType option.ActionName, actionCoordinates []float64) error { if actionType == "" || len(actionCoordinates) == 0 { return nil } diff --git a/uixt/driver_handler.go b/uixt/driver_handler.go index f8012e20..86acb3e7 100644 --- a/uixt/driver_handler.go +++ b/uixt/driver_handler.go @@ -98,7 +98,7 @@ func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, raw return fromX, fromY, toX, toY, nil } -func preHandler_Swipe(driver IDriver, actionType option.ActionMethod, +func preHandler_Swipe(driver IDriver, actionType option.ActionName, options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) ( fromX, fromY, toX, toY float64, err error) { @@ -118,7 +118,7 @@ func preHandler_Swipe(driver IDriver, actionType option.ActionMethod, return fromX, fromY, toX, toY, nil } -func postHandler(driver IDriver, actionType option.ActionMethod, options *option.ActionOptions) error { +func postHandler(driver IDriver, actionType option.ActionName, options *option.ActionOptions) error { // save screenshot after action if options.PostMarkOperation { // get compressed screenshot buffer diff --git a/uixt/mcp_server.go b/uixt/mcp_server.go index 953b605b..7e2fb3d3 100644 --- a/uixt/mcp_server.go +++ b/uixt/mcp_server.go @@ -45,7 +45,7 @@ func NewMCPServer() *MCPServer4XTDriver { ) s := &MCPServer4XTDriver{ mcpServer: mcpServer, - actionToolMap: make(map[option.ActionMethod]ActionTool), + actionToolMap: make(map[option.ActionName]ActionTool), } s.registerTools() return s @@ -54,8 +54,8 @@ func NewMCPServer() *MCPServer4XTDriver { // MCPServer4XTDriver wraps a MCPServer to expose XTDriver functionality via MCP protocol. type MCPServer4XTDriver struct { mcpServer *server.MCPServer - mcpTools []mcp.Tool // tools list for uixt - actionToolMap map[option.ActionMethod]ActionTool // action method to tool mapping + mcpTools []mcp.Tool // tools list for uixt + actionToolMap map[option.ActionName]ActionTool // action method to tool mapping } // Start runs the MCP server (blocking). @@ -80,7 +80,7 @@ func (s *MCPServer4XTDriver) GetTool(name string) *mcp.Tool { } // GetToolByAction returns the tool that handles the given action method -func (s *MCPServer4XTDriver) GetToolByAction(actionMethod option.ActionMethod) ActionTool { +func (s *MCPServer4XTDriver) GetToolByAction(actionMethod option.ActionName) ActionTool { if s.actionToolMap == nil { return nil } @@ -174,7 +174,7 @@ func (s *MCPServer4XTDriver) registerTool(tool ActionTool) { // ActionTool interface defines the contract for MCP tools type ActionTool interface { - Name() option.ActionMethod + Name() option.ActionName Description() string Options() []mcp.ToolOption Implement() server.ToolHandlerFunc @@ -183,7 +183,7 @@ type ActionTool interface { } // buildMCPCallToolRequest is a helper function to build mcp.CallToolRequest -func buildMCPCallToolRequest(toolName option.ActionMethod, arguments map[string]any) mcp.CallToolRequest { +func buildMCPCallToolRequest(toolName option.ActionName, arguments map[string]any) mcp.CallToolRequest { return mcp.CallToolRequest{ Params: struct { Name string `json:"name"` @@ -201,7 +201,7 @@ func buildMCPCallToolRequest(toolName option.ActionMethod, arguments map[string] // ToolListAvailableDevices implements the list_available_devices tool call. type ToolListAvailableDevices struct{} -func (t *ToolListAvailableDevices) Name() option.ActionMethod { +func (t *ToolListAvailableDevices) Name() option.ActionName { return option.ACTION_ListAvailableDevices } @@ -256,7 +256,7 @@ func (t *ToolListAvailableDevices) ConvertActionToCallToolRequest(action MobileA // ToolSelectDevice implements the select_device tool call. type ToolSelectDevice struct{} -func (t *ToolSelectDevice) Name() option.ActionMethod { +func (t *ToolSelectDevice) Name() option.ActionName { return option.ACTION_SelectDevice } @@ -290,7 +290,7 @@ func (t *ToolSelectDevice) ConvertActionToCallToolRequest(action MobileAction) ( // ToolTapXY implements the tap_xy tool call. type ToolTapXY struct{} -func (t *ToolTapXY) Name() option.ActionMethod { +func (t *ToolTapXY) Name() option.ActionName { return option.ACTION_TapXY } @@ -299,7 +299,7 @@ func (t *ToolTapXY) Description() string { } func (t *ToolTapXY) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_TapXY) } @@ -310,32 +310,31 @@ func (t *ToolTapXY) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - // Convert to ActionOptions - actionOpts := unifiedReq.ToActionOptions() - opts := actionOpts.Options() + // Get options directly since ActionOptions is now ActionOptions + opts := unifiedReq.Options() // Add default options opts = append(opts, option.WithPreMarkOperation(true)) // Validate required parameters - if unifiedReq.X == nil || unifiedReq.Y == nil { + if unifiedReq.X == 0 || unifiedReq.Y == 0 { return nil, fmt.Errorf("x and y coordinates are required") } // Tap action logic - log.Info().Float64("x", *unifiedReq.X).Float64("y", *unifiedReq.Y).Msg("tapping at coordinates") + log.Info().Float64("x", unifiedReq.X).Float64("y", unifiedReq.Y).Msg("tapping at coordinates") - err = driverExt.TapXY(*unifiedReq.X, *unifiedReq.Y, opts...) + err = driverExt.TapXY(unifiedReq.X, unifiedReq.Y, opts...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Tap failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped at coordinates (%.2f, %.2f)", *unifiedReq.X, *unifiedReq.Y)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped at coordinates (%.2f, %.2f)", unifiedReq.X, unifiedReq.Y)), nil } } @@ -362,7 +361,7 @@ func (t *ToolTapXY) ConvertActionToCallToolRequest(action MobileAction) (mcp.Cal // ToolTapAbsXY implements the tap_abs_xy tool call. type ToolTapAbsXY struct{} -func (t *ToolTapAbsXY) Name() option.ActionMethod { +func (t *ToolTapAbsXY) Name() option.ActionName { return option.ACTION_TapAbsXY } @@ -371,7 +370,7 @@ func (t *ToolTapAbsXY) Description() string { } func (t *ToolTapAbsXY) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_TapAbsXY) } @@ -382,32 +381,31 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - // Convert to ActionOptions - actionOpts := unifiedReq.ToActionOptions() - opts := actionOpts.Options() + // Get options directly since ActionOptions is now ActionOptions + opts := unifiedReq.Options() // Add default options opts = append(opts, option.WithPreMarkOperation(true)) // Validate required parameters - if unifiedReq.X == nil || unifiedReq.Y == nil { + if unifiedReq.X == 0 || unifiedReq.Y == 0 { return nil, fmt.Errorf("x and y coordinates are required") } // Tap absolute XY action logic - log.Info().Float64("x", *unifiedReq.X).Float64("y", *unifiedReq.Y).Msg("tapping at absolute coordinates") + log.Info().Float64("x", unifiedReq.X).Float64("y", unifiedReq.Y).Msg("tapping at absolute coordinates") - err = driverExt.TapAbsXY(*unifiedReq.X, *unifiedReq.Y, opts...) + err = driverExt.TapAbsXY(unifiedReq.X, unifiedReq.Y, opts...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Tap absolute XY failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped at absolute coordinates (%.0f, %.0f)", *unifiedReq.X, *unifiedReq.Y)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped at absolute coordinates (%.0f, %.0f)", unifiedReq.X, unifiedReq.Y)), nil } } @@ -434,7 +432,7 @@ func (t *ToolTapAbsXY) ConvertActionToCallToolRequest(action MobileAction) (mcp. // ToolTapByOCR implements the tap_ocr tool call. type ToolTapByOCR struct{} -func (t *ToolTapByOCR) Name() option.ActionMethod { +func (t *ToolTapByOCR) Name() option.ActionName { return option.ACTION_TapByOCR } @@ -443,7 +441,7 @@ func (t *ToolTapByOCR) Description() string { } func (t *ToolTapByOCR) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_TapByOCR) } @@ -454,14 +452,13 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - // Convert to ActionOptions - actionOpts := unifiedReq.ToActionOptions() - opts := actionOpts.Options() + // Get options directly since ActionOptions is now ActionOptions + opts := unifiedReq.Options() // Add default options opts = append(opts, option.WithPreMarkOperation(true)) @@ -499,7 +496,7 @@ func (t *ToolTapByOCR) ConvertActionToCallToolRequest(action MobileAction) (mcp. // ToolTapByCV implements the tap_cv tool call. type ToolTapByCV struct{} -func (t *ToolTapByCV) Name() option.ActionMethod { +func (t *ToolTapByCV) Name() option.ActionName { return option.ACTION_TapByCV } @@ -508,7 +505,7 @@ func (t *ToolTapByCV) Description() string { } func (t *ToolTapByCV) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_TapByCV) } @@ -519,14 +516,13 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - // Convert to ActionOptions - actionOpts := unifiedReq.ToActionOptions() - opts := actionOpts.Options() + // Get options directly since ActionOptions is now ActionOptions + opts := unifiedReq.Options() // Add default options opts = append(opts, option.WithPreMarkOperation(true)) @@ -561,7 +557,7 @@ func (t *ToolTapByCV) ConvertActionToCallToolRequest(action MobileAction) (mcp.C // ToolDoubleTapXY implements the double_tap_xy tool call. type ToolDoubleTapXY struct{} -func (t *ToolDoubleTapXY) Name() option.ActionMethod { +func (t *ToolDoubleTapXY) Name() option.ActionName { return option.ACTION_DoubleTapXY } @@ -570,7 +566,7 @@ func (t *ToolDoubleTapXY) Description() string { } func (t *ToolDoubleTapXY) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_DoubleTapXY) } @@ -581,24 +577,24 @@ func (t *ToolDoubleTapXY) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Validate required parameters - if unifiedReq.X == nil || unifiedReq.Y == nil { + if unifiedReq.X == 0 || unifiedReq.Y == 0 { return nil, fmt.Errorf("x and y coordinates are required") } // Double tap XY action logic - log.Info().Float64("x", *unifiedReq.X).Float64("y", *unifiedReq.Y).Msg("double tapping at coordinates") - err = driverExt.DoubleTap(*unifiedReq.X, *unifiedReq.Y) + log.Info().Float64("x", unifiedReq.X).Float64("y", unifiedReq.Y).Msg("double tapping at coordinates") + err = driverExt.DoubleTap(unifiedReq.X, unifiedReq.Y) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Double tap failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully double tapped at (%.2f, %.2f)", *unifiedReq.X, *unifiedReq.Y)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully double tapped at (%.2f, %.2f)", unifiedReq.X, unifiedReq.Y)), nil } } @@ -617,7 +613,7 @@ func (t *ToolDoubleTapXY) ConvertActionToCallToolRequest(action MobileAction) (m // ToolListPackages implements the list_packages tool call. type ToolListPackages struct{} -func (t *ToolListPackages) Name() option.ActionMethod { +func (t *ToolListPackages) Name() option.ActionName { return option.ACTION_ListPackages } @@ -626,7 +622,7 @@ func (t *ToolListPackages) Description() string { } func (t *ToolListPackages) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_ListPackages) } @@ -652,7 +648,7 @@ func (t *ToolListPackages) ConvertActionToCallToolRequest(action MobileAction) ( // ToolLaunchApp implements the launch_app tool call. type ToolLaunchApp struct{} -func (t *ToolLaunchApp) Name() option.ActionMethod { +func (t *ToolLaunchApp) Name() option.ActionName { return option.ACTION_AppLaunch } @@ -661,7 +657,7 @@ func (t *ToolLaunchApp) Description() string { } func (t *ToolLaunchApp) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_AppLaunch) } @@ -672,7 +668,7 @@ func (t *ToolLaunchApp) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -705,7 +701,7 @@ func (t *ToolLaunchApp) ConvertActionToCallToolRequest(action MobileAction) (mcp // ToolTerminateApp implements the terminate_app tool call. type ToolTerminateApp struct{} -func (t *ToolTerminateApp) Name() option.ActionMethod { +func (t *ToolTerminateApp) Name() option.ActionName { return option.ACTION_AppTerminate } @@ -714,7 +710,7 @@ func (t *ToolTerminateApp) Description() string { } func (t *ToolTerminateApp) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_AppTerminate) } @@ -725,7 +721,7 @@ func (t *ToolTerminateApp) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -761,7 +757,7 @@ func (t *ToolTerminateApp) ConvertActionToCallToolRequest(action MobileAction) ( // ToolScreenShot implements the screenshot tool call. type ToolScreenShot struct{} -func (t *ToolScreenShot) Name() option.ActionMethod { +func (t *ToolScreenShot) Name() option.ActionName { return option.ACTION_ScreenShot } @@ -770,7 +766,7 @@ func (t *ToolScreenShot) Description() string { } func (t *ToolScreenShot) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_ScreenShot) } @@ -798,7 +794,7 @@ func (t *ToolScreenShot) ConvertActionToCallToolRequest(action MobileAction) (mc // ToolGetScreenSize implements the get_screen_size tool call. type ToolGetScreenSize struct{} -func (t *ToolGetScreenSize) Name() option.ActionMethod { +func (t *ToolGetScreenSize) Name() option.ActionName { return option.ACTION_GetScreenSize } @@ -807,7 +803,7 @@ func (t *ToolGetScreenSize) Description() string { } func (t *ToolGetScreenSize) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_GetScreenSize) } @@ -835,7 +831,7 @@ func (t *ToolGetScreenSize) ConvertActionToCallToolRequest(action MobileAction) // ToolPressButton implements the press_button tool call. type ToolPressButton struct{} -func (t *ToolPressButton) Name() option.ActionMethod { +func (t *ToolPressButton) Name() option.ActionName { return option.ACTION_PressButton } @@ -844,7 +840,7 @@ func (t *ToolPressButton) Description() string { } func (t *ToolPressButton) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_PressButton) } @@ -855,7 +851,7 @@ func (t *ToolPressButton) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -886,7 +882,7 @@ func (t *ToolPressButton) ConvertActionToCallToolRequest(action MobileAction) (m // based on the params type. type ToolSwipe struct{} -func (t *ToolSwipe) Name() option.ActionMethod { +func (t *ToolSwipe) Name() option.ActionName { return option.ACTION_Swipe } @@ -895,7 +891,7 @@ func (t *ToolSwipe) Description() string { } func (t *ToolSwipe) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_Swipe) } @@ -947,7 +943,7 @@ func (t *ToolSwipe) ConvertActionToCallToolRequest(action MobileAction) (mcp.Cal // ToolSwipeDirection implements the swipe tool call. type ToolSwipeDirection struct{} -func (t *ToolSwipeDirection) Name() option.ActionMethod { +func (t *ToolSwipeDirection) Name() option.ActionName { return option.ACTION_SwipeDirection } @@ -956,7 +952,7 @@ func (t *ToolSwipeDirection) Description() string { } func (t *ToolSwipeDirection) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SwipeDirection) } @@ -967,13 +963,13 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Swipe action logic - log.Info().Str("direction", unifiedReq.Direction).Msg("performing swipe") + log.Info().Interface("direction", unifiedReq.Direction).Msg("performing swipe") // Validate direction validDirections := []string{"up", "down", "left", "right"} @@ -990,8 +986,8 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc { opts := []option.ActionOption{ option.WithPreMarkOperation(true), - option.WithDuration(getFloat64ValueOrDefault(unifiedReq.Duration, 0.5)), - option.WithPressDuration(getFloat64ValueOrDefault(unifiedReq.PressDuration, 0.1)), + option.WithDuration(getFloat64ValueOrDefault(&unifiedReq.Duration, 0.5)), + option.WithPressDuration(getFloat64ValueOrDefault(&unifiedReq.PressDuration, 0.1)), } // Convert direction to coordinates and perform swipe @@ -1037,7 +1033,7 @@ func (t *ToolSwipeDirection) ConvertActionToCallToolRequest(action MobileAction) // ToolSwipeCoordinate implements the swipe_advanced tool call. type ToolSwipeCoordinate struct{} -func (t *ToolSwipeCoordinate) Name() option.ActionMethod { +func (t *ToolSwipeCoordinate) Name() option.ActionName { return option.ACTION_SwipeCoordinate } @@ -1046,7 +1042,7 @@ func (t *ToolSwipeCoordinate) Description() string { } func (t *ToolSwipeCoordinate) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SwipeCoordinate) } @@ -1057,29 +1053,29 @@ func (t *ToolSwipeCoordinate) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Validate required parameters - if unifiedReq.FromX == nil || unifiedReq.FromY == nil || unifiedReq.ToX == nil || unifiedReq.ToY == nil { + if unifiedReq.FromX == 0 || unifiedReq.FromY == 0 || unifiedReq.ToX == 0 || unifiedReq.ToY == 0 { return nil, fmt.Errorf("fromX, fromY, toX, and toY coordinates are required") } // Advanced swipe action logic using prepareSwipeAction like the original DoAction log.Info(). - Float64("fromX", *unifiedReq.FromX).Float64("fromY", *unifiedReq.FromY). - Float64("toX", *unifiedReq.ToX).Float64("toY", *unifiedReq.ToY). + Float64("fromX", unifiedReq.FromX).Float64("fromY", unifiedReq.FromY). + Float64("toX", unifiedReq.ToX).Float64("toY", unifiedReq.ToY). Msg("performing advanced swipe") - params := []float64{*unifiedReq.FromX, *unifiedReq.FromY, *unifiedReq.ToX, *unifiedReq.ToY} + params := []float64{unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY} opts := []option.ActionOption{} - if unifiedReq.Duration != nil && *unifiedReq.Duration > 0 { - opts = append(opts, option.WithDuration(*unifiedReq.Duration)) + if unifiedReq.Duration > 0 && unifiedReq.Duration > 0 { + opts = append(opts, option.WithDuration(unifiedReq.Duration)) } - if unifiedReq.PressDuration != nil && *unifiedReq.PressDuration > 0 { - opts = append(opts, option.WithPressDuration(*unifiedReq.PressDuration)) + if unifiedReq.PressDuration > 0 && unifiedReq.PressDuration > 0 { + opts = append(opts, option.WithPressDuration(unifiedReq.PressDuration)) } swipeAction := prepareSwipeAction(driverExt, params, opts...) @@ -1089,7 +1085,7 @@ func (t *ToolSwipeCoordinate) Implement() server.ToolHandlerFunc { } return mcp.NewToolResultText(fmt.Sprintf("Successfully performed advanced swipe from (%.2f, %.2f) to (%.2f, %.2f)", - *unifiedReq.FromX, *unifiedReq.FromY, *unifiedReq.ToX, *unifiedReq.ToY)), nil + unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY)), nil } } @@ -1116,7 +1112,7 @@ func (t *ToolSwipeCoordinate) ConvertActionToCallToolRequest(action MobileAction // ToolSwipeToTapApp implements the swipe_to_tap_app tool call. type ToolSwipeToTapApp struct{} -func (t *ToolSwipeToTapApp) Name() option.ActionMethod { +func (t *ToolSwipeToTapApp) Name() option.ActionName { return option.ACTION_SwipeToTapApp } @@ -1125,7 +1121,7 @@ func (t *ToolSwipeToTapApp) Description() string { } func (t *ToolSwipeToTapApp) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SwipeToTapApp) } @@ -1136,7 +1132,7 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1145,16 +1141,16 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc { var opts []option.ActionOption // Add boolean options - if getBoolValue(unifiedReq.IgnoreNotFoundError) { + if unifiedReq.IgnoreNotFoundError { opts = append(opts, option.WithIgnoreNotFoundError(true)) } // Add numeric options - if unifiedReq.MaxRetryTimes != nil && *unifiedReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(*unifiedReq.MaxRetryTimes)) + if unifiedReq.MaxRetryTimes > 0 && unifiedReq.MaxRetryTimes > 0 { + opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes)) } - if unifiedReq.Index != nil && *unifiedReq.Index > 0 { - opts = append(opts, option.WithIndex(*unifiedReq.Index)) + if unifiedReq.Index > 0 && unifiedReq.Index > 0 { + opts = append(opts, option.WithIndex(unifiedReq.Index)) } // Swipe to tap app action logic @@ -1185,7 +1181,7 @@ func (t *ToolSwipeToTapApp) ConvertActionToCallToolRequest(action MobileAction) // ToolSwipeToTapText implements the swipe_to_tap_text tool call. type ToolSwipeToTapText struct{} -func (t *ToolSwipeToTapText) Name() option.ActionMethod { +func (t *ToolSwipeToTapText) Name() option.ActionName { return option.ACTION_SwipeToTapText } @@ -1194,7 +1190,7 @@ func (t *ToolSwipeToTapText) Description() string { } func (t *ToolSwipeToTapText) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SwipeToTapText) } @@ -1205,7 +1201,7 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1214,19 +1210,19 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc { var opts []option.ActionOption // Add boolean options - if getBoolValue(unifiedReq.IgnoreNotFoundError) { + if unifiedReq.IgnoreNotFoundError { opts = append(opts, option.WithIgnoreNotFoundError(true)) } - if getBoolValue(unifiedReq.Regex) { + if unifiedReq.Regex { opts = append(opts, option.WithRegex(true)) } // Add numeric options - if unifiedReq.MaxRetryTimes != nil && *unifiedReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(*unifiedReq.MaxRetryTimes)) + if unifiedReq.MaxRetryTimes > 0 && unifiedReq.MaxRetryTimes > 0 { + opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes)) } - if unifiedReq.Index != nil && *unifiedReq.Index > 0 { - opts = append(opts, option.WithIndex(*unifiedReq.Index)) + if unifiedReq.Index > 0 && unifiedReq.Index > 0 { + opts = append(opts, option.WithIndex(unifiedReq.Index)) } // Swipe to tap text action logic @@ -1257,7 +1253,7 @@ func (t *ToolSwipeToTapText) ConvertActionToCallToolRequest(action MobileAction) // ToolSwipeToTapTexts implements the swipe_to_tap_texts tool call. type ToolSwipeToTapTexts struct{} -func (t *ToolSwipeToTapTexts) Name() option.ActionMethod { +func (t *ToolSwipeToTapTexts) Name() option.ActionName { return option.ACTION_SwipeToTapTexts } @@ -1266,7 +1262,7 @@ func (t *ToolSwipeToTapTexts) Description() string { } func (t *ToolSwipeToTapTexts) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SwipeToTapTexts) } @@ -1277,7 +1273,7 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1286,19 +1282,19 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc { var opts []option.ActionOption // Add boolean options - if getBoolValue(unifiedReq.IgnoreNotFoundError) { + if unifiedReq.IgnoreNotFoundError { opts = append(opts, option.WithIgnoreNotFoundError(true)) } - if getBoolValue(unifiedReq.Regex) { + if unifiedReq.Regex { opts = append(opts, option.WithRegex(true)) } // Add numeric options - if unifiedReq.MaxRetryTimes != nil && *unifiedReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(*unifiedReq.MaxRetryTimes)) + if unifiedReq.MaxRetryTimes > 0 && unifiedReq.MaxRetryTimes > 0 { + opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes)) } - if unifiedReq.Index != nil && *unifiedReq.Index > 0 { - opts = append(opts, option.WithIndex(*unifiedReq.Index)) + if unifiedReq.Index > 0 && unifiedReq.Index > 0 { + opts = append(opts, option.WithIndex(unifiedReq.Index)) } // Swipe to tap texts action logic @@ -1334,7 +1330,7 @@ func (t *ToolSwipeToTapTexts) ConvertActionToCallToolRequest(action MobileAction // ToolDrag implements the drag tool call. type ToolDrag struct{} -func (t *ToolDrag) Name() option.ActionMethod { +func (t *ToolDrag) Name() option.ActionName { return option.ACTION_Drag } @@ -1343,7 +1339,7 @@ func (t *ToolDrag) Description() string { } func (t *ToolDrag) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_Drag) } @@ -1354,34 +1350,34 @@ func (t *ToolDrag) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Validate required parameters - if unifiedReq.FromX == nil || unifiedReq.FromY == nil || unifiedReq.ToX == nil || unifiedReq.ToY == nil { + if unifiedReq.FromX == 0 || unifiedReq.FromY == 0 || unifiedReq.ToX == 0 || unifiedReq.ToY == 0 { return nil, fmt.Errorf("fromX, fromY, toX, and toY coordinates are required") } opts := []option.ActionOption{} - if unifiedReq.Duration != nil && *unifiedReq.Duration > 0 { - opts = append(opts, option.WithDuration(*unifiedReq.Duration/1000.0)) + if unifiedReq.Duration > 0 { + opts = append(opts, option.WithDuration(unifiedReq.Duration/1000.0)) } // Drag action logic log.Info(). - Float64("fromX", *unifiedReq.FromX).Float64("fromY", *unifiedReq.FromY). - Float64("toX", *unifiedReq.ToX).Float64("toY", *unifiedReq.ToY). + Float64("fromX", unifiedReq.FromX).Float64("fromY", unifiedReq.FromY). + Float64("toX", unifiedReq.ToX).Float64("toY", unifiedReq.ToY). Msg("performing drag") - err = driverExt.Swipe(*unifiedReq.FromX, *unifiedReq.FromY, *unifiedReq.ToX, *unifiedReq.ToY, opts...) + err = driverExt.Swipe(unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY, opts...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Drag failed: %s", err.Error())), nil } return mcp.NewToolResultText(fmt.Sprintf("Successfully dragged from (%.2f, %.2f) to (%.2f, %.2f)", - *unifiedReq.FromX, *unifiedReq.FromY, *unifiedReq.ToX, *unifiedReq.ToY)), nil + unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY)), nil } } @@ -1456,7 +1452,7 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume // ToolHome implements the home tool call. type ToolHome struct{} -func (t *ToolHome) Name() option.ActionMethod { +func (t *ToolHome) Name() option.ActionName { return option.ACTION_Home } @@ -1465,7 +1461,7 @@ func (t *ToolHome) Description() string { } func (t *ToolHome) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_Home) } @@ -1494,7 +1490,7 @@ func (t *ToolHome) ConvertActionToCallToolRequest(action MobileAction) (mcp.Call // ToolBack implements the back tool call. type ToolBack struct{} -func (t *ToolBack) Name() option.ActionMethod { +func (t *ToolBack) Name() option.ActionName { return option.ACTION_Back } @@ -1503,7 +1499,7 @@ func (t *ToolBack) Description() string { } func (t *ToolBack) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_Back) } @@ -1532,7 +1528,7 @@ func (t *ToolBack) ConvertActionToCallToolRequest(action MobileAction) (mcp.Call // ToolInput implements the input tool call. type ToolInput struct{} -func (t *ToolInput) Name() option.ActionMethod { +func (t *ToolInput) Name() option.ActionName { return option.ACTION_Input } @@ -1541,7 +1537,7 @@ func (t *ToolInput) Description() string { } func (t *ToolInput) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_Input) } @@ -1552,7 +1548,7 @@ func (t *ToolInput) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1583,7 +1579,7 @@ func (t *ToolInput) ConvertActionToCallToolRequest(action MobileAction) (mcp.Cal // ToolWebLoginNoneUI implements the web_login_none_ui tool call. type ToolWebLoginNoneUI struct{} -func (t *ToolWebLoginNoneUI) Name() option.ActionMethod { +func (t *ToolWebLoginNoneUI) Name() option.ActionName { return option.ACTION_WebLoginNoneUI } @@ -1592,7 +1588,7 @@ func (t *ToolWebLoginNoneUI) Description() string { } func (t *ToolWebLoginNoneUI) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_WebLoginNoneUI) } @@ -1603,7 +1599,7 @@ func (t *ToolWebLoginNoneUI) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1631,7 +1627,7 @@ func (t *ToolWebLoginNoneUI) ConvertActionToCallToolRequest(action MobileAction) // ToolAppInstall implements the app_install tool call. type ToolAppInstall struct{} -func (t *ToolAppInstall) Name() option.ActionMethod { +func (t *ToolAppInstall) Name() option.ActionName { return option.ACTION_AppInstall } @@ -1640,7 +1636,7 @@ func (t *ToolAppInstall) Description() string { } func (t *ToolAppInstall) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_AppInstall) } @@ -1651,7 +1647,7 @@ func (t *ToolAppInstall) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1680,7 +1676,7 @@ func (t *ToolAppInstall) ConvertActionToCallToolRequest(action MobileAction) (mc // ToolAppUninstall implements the app_uninstall tool call. type ToolAppUninstall struct{} -func (t *ToolAppUninstall) Name() option.ActionMethod { +func (t *ToolAppUninstall) Name() option.ActionName { return option.ACTION_AppUninstall } @@ -1689,7 +1685,7 @@ func (t *ToolAppUninstall) Description() string { } func (t *ToolAppUninstall) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_AppUninstall) } @@ -1700,7 +1696,7 @@ func (t *ToolAppUninstall) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1729,7 +1725,7 @@ func (t *ToolAppUninstall) ConvertActionToCallToolRequest(action MobileAction) ( // ToolAppClear implements the app_clear tool call. type ToolAppClear struct{} -func (t *ToolAppClear) Name() option.ActionMethod { +func (t *ToolAppClear) Name() option.ActionName { return option.ACTION_AppClear } @@ -1738,7 +1734,7 @@ func (t *ToolAppClear) Description() string { } func (t *ToolAppClear) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_AppClear) } @@ -1749,7 +1745,7 @@ func (t *ToolAppClear) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1778,7 +1774,7 @@ func (t *ToolAppClear) ConvertActionToCallToolRequest(action MobileAction) (mcp. // ToolSecondaryClick implements the secondary_click tool call. type ToolSecondaryClick struct{} -func (t *ToolSecondaryClick) Name() option.ActionMethod { +func (t *ToolSecondaryClick) Name() option.ActionName { return option.ACTION_SecondaryClick } @@ -1787,7 +1783,7 @@ func (t *ToolSecondaryClick) Description() string { } func (t *ToolSecondaryClick) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SecondaryClick) } @@ -1798,24 +1794,24 @@ func (t *ToolSecondaryClick) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Validate required parameters - if unifiedReq.X == nil || unifiedReq.Y == nil { + if unifiedReq.X == 0 || unifiedReq.Y == 0 { return nil, fmt.Errorf("x and y coordinates are required") } // Secondary click action logic - log.Info().Float64("x", *unifiedReq.X).Float64("y", *unifiedReq.Y).Msg("performing secondary click") - err = driverExt.SecondaryClick(*unifiedReq.X, *unifiedReq.Y) + log.Info().Float64("x", unifiedReq.X).Float64("y", unifiedReq.Y).Msg("performing secondary click") + err = driverExt.SecondaryClick(unifiedReq.X, unifiedReq.Y) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Secondary click failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully performed secondary click at (%.2f, %.2f)", *unifiedReq.X, *unifiedReq.Y)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully performed secondary click at (%.2f, %.2f)", unifiedReq.X, unifiedReq.Y)), nil } } @@ -1833,7 +1829,7 @@ func (t *ToolSecondaryClick) ConvertActionToCallToolRequest(action MobileAction) // ToolHoverBySelector implements the hover_by_selector tool call. type ToolHoverBySelector struct{} -func (t *ToolHoverBySelector) Name() option.ActionMethod { +func (t *ToolHoverBySelector) Name() option.ActionName { return option.ACTION_HoverBySelector } @@ -1842,7 +1838,7 @@ func (t *ToolHoverBySelector) Description() string { } func (t *ToolHoverBySelector) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_HoverBySelector) } @@ -1853,7 +1849,7 @@ func (t *ToolHoverBySelector) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1882,7 +1878,7 @@ func (t *ToolHoverBySelector) ConvertActionToCallToolRequest(action MobileAction // ToolTapBySelector implements the tap_by_selector tool call. type ToolTapBySelector struct{} -func (t *ToolTapBySelector) Name() option.ActionMethod { +func (t *ToolTapBySelector) Name() option.ActionName { return option.ACTION_TapBySelector } @@ -1891,7 +1887,7 @@ func (t *ToolTapBySelector) Description() string { } func (t *ToolTapBySelector) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_TapBySelector) } @@ -1902,7 +1898,7 @@ func (t *ToolTapBySelector) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1931,7 +1927,7 @@ func (t *ToolTapBySelector) ConvertActionToCallToolRequest(action MobileAction) // ToolSecondaryClickBySelector implements the secondary_click_by_selector tool call. type ToolSecondaryClickBySelector struct{} -func (t *ToolSecondaryClickBySelector) Name() option.ActionMethod { +func (t *ToolSecondaryClickBySelector) Name() option.ActionName { return option.ACTION_SecondaryClickBySelector } @@ -1940,7 +1936,7 @@ func (t *ToolSecondaryClickBySelector) Description() string { } func (t *ToolSecondaryClickBySelector) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SecondaryClickBySelector) } @@ -1951,7 +1947,7 @@ func (t *ToolSecondaryClickBySelector) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1980,7 +1976,7 @@ func (t *ToolSecondaryClickBySelector) ConvertActionToCallToolRequest(action Mob // ToolWebCloseTab implements the web_close_tab tool call. type ToolWebCloseTab struct{} -func (t *ToolWebCloseTab) Name() option.ActionMethod { +func (t *ToolWebCloseTab) Name() option.ActionName { return option.ACTION_WebCloseTab } @@ -1989,7 +1985,7 @@ func (t *ToolWebCloseTab) Description() string { } func (t *ToolWebCloseTab) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_WebCloseTab) } @@ -2000,24 +1996,24 @@ func (t *ToolWebCloseTab) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Validate required parameters - if unifiedReq.TabIndex == nil { + if unifiedReq.TabIndex == 0 { return nil, fmt.Errorf("tabIndex is required") } // Web close tab action logic - log.Info().Int("tabIndex", *unifiedReq.TabIndex).Msg("closing web tab") + log.Info().Int("tabIndex", unifiedReq.TabIndex).Msg("closing web tab") browserDriver, ok := driverExt.IDriver.(*BrowserDriver) if !ok { return nil, fmt.Errorf("web close tab is only supported for browser drivers") } - err = browserDriver.CloseTab(*unifiedReq.TabIndex) + err = browserDriver.CloseTab(unifiedReq.TabIndex) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Close tab failed: %s", err.Error())), nil } @@ -2047,7 +2043,7 @@ func (t *ToolWebCloseTab) ConvertActionToCallToolRequest(action MobileAction) (m // ToolSetIme implements the set_ime tool call. type ToolSetIme struct{} -func (t *ToolSetIme) Name() option.ActionMethod { +func (t *ToolSetIme) Name() option.ActionName { return option.ACTION_SetIme } @@ -2056,7 +2052,7 @@ func (t *ToolSetIme) Description() string { } func (t *ToolSetIme) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SetIme) } @@ -2067,7 +2063,7 @@ func (t *ToolSetIme) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -2096,7 +2092,7 @@ func (t *ToolSetIme) ConvertActionToCallToolRequest(action MobileAction) (mcp.Ca // ToolGetSource implements the get_source tool call. type ToolGetSource struct{} -func (t *ToolGetSource) Name() option.ActionMethod { +func (t *ToolGetSource) Name() option.ActionName { return option.ACTION_GetSource } @@ -2105,7 +2101,7 @@ func (t *ToolGetSource) Description() string { } func (t *ToolGetSource) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_GetSource) } @@ -2116,7 +2112,7 @@ func (t *ToolGetSource) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -2145,7 +2141,7 @@ func (t *ToolGetSource) ConvertActionToCallToolRequest(action MobileAction) (mcp // ToolSleep implements the sleep tool call. type ToolSleep struct{} -func (t *ToolSleep) Name() option.ActionMethod { +func (t *ToolSleep) Name() option.ActionName { return option.ACTION_Sleep } @@ -2203,7 +2199,7 @@ func (t *ToolSleep) ConvertActionToCallToolRequest(action MobileAction) (mcp.Cal // ToolSleepMS implements the sleep_ms tool call. type ToolSleepMS struct{} -func (t *ToolSleepMS) Name() option.ActionMethod { +func (t *ToolSleepMS) Name() option.ActionName { return option.ACTION_SleepMS } @@ -2212,27 +2208,27 @@ func (t *ToolSleepMS) Description() string { } func (t *ToolSleepMS) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SleepMS) } func (t *ToolSleepMS) Implement() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Validate required parameters - if unifiedReq.Milliseconds == nil { + if unifiedReq.Milliseconds == 0 { return nil, fmt.Errorf("milliseconds is required") } // Sleep MS action logic - log.Info().Int64("milliseconds", *unifiedReq.Milliseconds).Msg("sleeping in milliseconds") - time.Sleep(time.Duration(*unifiedReq.Milliseconds) * time.Millisecond) + log.Info().Int64("milliseconds", unifiedReq.Milliseconds).Msg("sleeping in milliseconds") + time.Sleep(time.Duration(unifiedReq.Milliseconds) * time.Millisecond) - return mcp.NewToolResultText(fmt.Sprintf("Successfully slept for %d milliseconds", *unifiedReq.Milliseconds)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully slept for %d milliseconds", unifiedReq.Milliseconds)), nil } } @@ -2254,7 +2250,7 @@ func (t *ToolSleepMS) ConvertActionToCallToolRequest(action MobileAction) (mcp.C // ToolSleepRandom implements the sleep_random tool call. type ToolSleepRandom struct{} -func (t *ToolSleepRandom) Name() option.ActionMethod { +func (t *ToolSleepRandom) Name() option.ActionName { return option.ACTION_SleepRandom } @@ -2263,13 +2259,13 @@ func (t *ToolSleepRandom) Description() string { } func (t *ToolSleepRandom) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_SleepRandom) } func (t *ToolSleepRandom) Implement() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -2295,7 +2291,7 @@ func (t *ToolSleepRandom) ConvertActionToCallToolRequest(action MobileAction) (m // ToolClosePopups implements the close_popups tool call. type ToolClosePopups struct{} -func (t *ToolClosePopups) Name() option.ActionMethod { +func (t *ToolClosePopups) Name() option.ActionName { return option.ACTION_ClosePopups } @@ -2304,7 +2300,7 @@ func (t *ToolClosePopups) Description() string { } func (t *ToolClosePopups) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_ClosePopups) } @@ -2333,7 +2329,7 @@ func (t *ToolClosePopups) ConvertActionToCallToolRequest(action MobileAction) (m // ToolAIAction implements the ai_action tool call. type ToolAIAction struct{} -func (t *ToolAIAction) Name() option.ActionMethod { +func (t *ToolAIAction) Name() option.ActionName { return option.ACTION_AIAction } @@ -2342,7 +2338,7 @@ func (t *ToolAIAction) Description() string { } func (t *ToolAIAction) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_AIAction) } @@ -2353,7 +2349,7 @@ func (t *ToolAIAction) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -2382,7 +2378,7 @@ func (t *ToolAIAction) ConvertActionToCallToolRequest(action MobileAction) (mcp. // ToolFinished implements the finished tool call. type ToolFinished struct{} -func (t *ToolFinished) Name() option.ActionMethod { +func (t *ToolFinished) Name() option.ActionName { return option.ACTION_Finished } @@ -2391,13 +2387,13 @@ func (t *ToolFinished) Description() string { } func (t *ToolFinished) Options() []mcp.ToolOption { - unifiedReq := &option.UnifiedActionRequest{} + unifiedReq := &option.ActionOptions{} return unifiedReq.GetMCPOptions(option.ACTION_Finished) } func (t *ToolFinished) Implement() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - var unifiedReq option.UnifiedActionRequest + var unifiedReq option.ActionOptions if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } diff --git a/uixt/option/action.go b/uixt/option/action.go index 9ebd3d30..108132c3 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -2,82 +2,87 @@ package option import ( "context" + "fmt" "math/rand/v2" + "reflect" + "strings" "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/uixt/types" + "github.com/mark3labs/mcp-go/mcp" "github.com/rs/zerolog/log" ) -type ActionMethod string +type ActionName string const ( - ACTION_LOG ActionMethod = "log" - ACTION_ListPackages ActionMethod = "list_packages" - ACTION_AppInstall ActionMethod = "app_install" - ACTION_AppUninstall ActionMethod = "app_uninstall" - ACTION_WebLoginNoneUI ActionMethod = "web_login_none_ui" - ACTION_AppClear ActionMethod = "app_clear" - ACTION_AppStart ActionMethod = "app_start" - ACTION_AppLaunch ActionMethod = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成 - ACTION_AppTerminate ActionMethod = "app_terminate" - ACTION_AppStop ActionMethod = "app_stop" - ACTION_ScreenShot ActionMethod = "screenshot" - ACTION_GetScreenSize ActionMethod = "get_screen_size" - ACTION_Sleep ActionMethod = "sleep" - ACTION_SleepMS ActionMethod = "sleep_ms" - ACTION_SleepRandom ActionMethod = "sleep_random" - ACTION_SetIme ActionMethod = "set_ime" - ACTION_GetSource ActionMethod = "get_source" - ACTION_GetForegroundApp ActionMethod = "get_foreground_app" + ACTION_LOG ActionName = "log" + ACTION_ListPackages ActionName = "list_packages" + ACTION_AppInstall ActionName = "app_install" + ACTION_AppUninstall ActionName = "app_uninstall" + ACTION_WebLoginNoneUI ActionName = "web_login_none_ui" + ACTION_AppClear ActionName = "app_clear" + ACTION_AppStart ActionName = "app_start" + ACTION_AppLaunch ActionName = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成 + ACTION_AppTerminate ActionName = "app_terminate" + ACTION_AppStop ActionName = "app_stop" + ACTION_ScreenShot ActionName = "screenshot" + ACTION_GetScreenSize ActionName = "get_screen_size" + ACTION_Sleep ActionName = "sleep" + ACTION_SleepMS ActionName = "sleep_ms" + ACTION_SleepRandom ActionName = "sleep_random" + ACTION_SetIme ActionName = "set_ime" + ACTION_GetSource ActionName = "get_source" + ACTION_GetForegroundApp ActionName = "get_foreground_app" // UI handling - ACTION_Home ActionMethod = "home" - ACTION_Tap ActionMethod = "tap" // generic tap action - ACTION_TapXY ActionMethod = "tap_xy" - ACTION_TapAbsXY ActionMethod = "tap_abs_xy" - ACTION_TapByOCR ActionMethod = "tap_ocr" - ACTION_TapByCV ActionMethod = "tap_cv" - ACTION_DoubleTap ActionMethod = "double_tap" // generic double tap action - ACTION_DoubleTapXY ActionMethod = "double_tap_xy" - ACTION_Swipe ActionMethod = "swipe" // swipe by direction or coordinates - ACTION_SwipeDirection ActionMethod = "swipe_direction" // swipe by direction (up, down, left, right) - ACTION_SwipeCoordinate ActionMethod = "swipe_coordinate" // swipe by coordinates (fromX, fromY, toX, toY) - ACTION_Drag ActionMethod = "drag" - ACTION_Input ActionMethod = "input" - ACTION_PressButton ActionMethod = "press_button" - ACTION_Back ActionMethod = "back" - ACTION_KeyCode ActionMethod = "keycode" - ACTION_Delete ActionMethod = "delete" // delete action - ACTION_Backspace ActionMethod = "backspace" // backspace action - ACTION_AIAction ActionMethod = "ai_action" // action with ai - ACTION_TapBySelector ActionMethod = "tap_by_selector" - ACTION_HoverBySelector ActionMethod = "hover_by_selector" - ACTION_Hover ActionMethod = "hover" // generic hover action - ACTION_RightClick ActionMethod = "right_click" // right click action - ACTION_WebCloseTab ActionMethod = "web_close_tab" - ACTION_SecondaryClick ActionMethod = "secondary_click" - ACTION_SecondaryClickBySelector ActionMethod = "secondary_click_by_selector" - ACTION_GetElementTextBySelector ActionMethod = "get_element_text_by_selector" - ACTION_Scroll ActionMethod = "scroll" // scroll action - ACTION_Upload ActionMethod = "upload" // upload action - ACTION_PushMedia ActionMethod = "push_media" // push media action - ACTION_CreateBrowser ActionMethod = "create_browser" // create browser action - ACTION_AppInfo ActionMethod = "app_info" // get app info action + ACTION_Home ActionName = "home" + ACTION_Tap ActionName = "tap" // generic tap action + ACTION_TapXY ActionName = "tap_xy" + ACTION_TapAbsXY ActionName = "tap_abs_xy" + ACTION_TapByOCR ActionName = "tap_ocr" + ACTION_TapByCV ActionName = "tap_cv" + ACTION_DoubleTap ActionName = "double_tap" // generic double tap action + ACTION_DoubleTapXY ActionName = "double_tap_xy" + ACTION_Swipe ActionName = "swipe" // swipe by direction or coordinates + ACTION_SwipeDirection ActionName = "swipe_direction" // swipe by direction (up, down, left, right) + ACTION_SwipeCoordinate ActionName = "swipe_coordinate" // swipe by coordinates (fromX, fromY, toX, toY) + ACTION_Drag ActionName = "drag" + ACTION_Input ActionName = "input" + ACTION_PressButton ActionName = "press_button" + ACTION_Back ActionName = "back" + ACTION_KeyCode ActionName = "keycode" + ACTION_Delete ActionName = "delete" // delete action + ACTION_Backspace ActionName = "backspace" // backspace action + ACTION_AIAction ActionName = "ai_action" // action with ai + ACTION_TapBySelector ActionName = "tap_by_selector" + ACTION_HoverBySelector ActionName = "hover_by_selector" + ACTION_Hover ActionName = "hover" // generic hover action + ACTION_RightClick ActionName = "right_click" // right click action + ACTION_WebCloseTab ActionName = "web_close_tab" + ACTION_SecondaryClick ActionName = "secondary_click" + ACTION_SecondaryClickBySelector ActionName = "secondary_click_by_selector" + ACTION_GetElementTextBySelector ActionName = "get_element_text_by_selector" + ACTION_Scroll ActionName = "scroll" // scroll action + ACTION_Upload ActionName = "upload" // upload action + ACTION_PushMedia ActionName = "push_media" // push media action + ACTION_CreateBrowser ActionName = "create_browser" // create browser action + ACTION_AppInfo ActionName = "app_info" // get app info action // device actions - ACTION_ListAvailableDevices ActionMethod = "list_available_devices" - ACTION_SelectDevice ActionMethod = "select_device" + ACTION_ListAvailableDevices ActionName = "list_available_devices" + ACTION_SelectDevice ActionName = "select_device" // custom actions - ACTION_SwipeToTapApp ActionMethod = "swipe_to_tap_app" // swipe left & right to find app and tap - ACTION_SwipeToTapText ActionMethod = "swipe_to_tap_text" // swipe up & down to find text and tap - ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap - ACTION_ClosePopups ActionMethod = "close_popups" - ACTION_EndToEndDelay ActionMethod = "live_e2e" - ACTION_InstallApp ActionMethod = "install_app" - ACTION_UninstallApp ActionMethod = "uninstall_app" - ACTION_DownloadApp ActionMethod = "download_app" - ACTION_Finished ActionMethod = "finished" + ACTION_SwipeToTapApp ActionName = "swipe_to_tap_app" // swipe left & right to find app and tap + ACTION_SwipeToTapText ActionName = "swipe_to_tap_text" // swipe up & down to find text and tap + ACTION_SwipeToTapTexts ActionName = "swipe_to_tap_texts" // swipe up & down to find text and tap + ACTION_ClosePopups ActionName = "close_popups" + ACTION_EndToEndDelay ActionName = "live_e2e" + ACTION_InstallApp ActionName = "install_app" + ACTION_UninstallApp ActionName = "uninstall_app" + ACTION_DownloadApp ActionName = "download_app" + ACTION_Finished ActionName = "finished" ) const ( @@ -99,24 +104,79 @@ const ( ) type ActionOptions struct { - Context context.Context `json:"-" yaml:"-"` - // log - Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log + // Device targeting + Platform string `json:"platform,omitempty" yaml:"platform,omitempty" binding:"omitempty" desc:"Device platform: android/ios/browser"` + Serial string `json:"serial,omitempty" yaml:"serial,omitempty" binding:"omitempty" desc:"Device serial/udid/browser id"` - // control related - MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times - Interval float64 `json:"interval,omitempty" yaml:"interval,omitempty"` // interval between retries in seconds - Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` // used to set duration in seconds - PressDuration float64 `json:"press_duration,omitempty" yaml:"press_duration,omitempty"` // used to set press duration in seconds - Steps int `json:"steps,omitempty" yaml:"steps,omitempty"` // used to set steps of action - Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app - Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action - Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty"` + // Common action parameters + X float64 `json:"x,omitempty" yaml:"x,omitempty" binding:"omitempty,min=0" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"` + Y float64 `json:"y,omitempty" yaml:"y,omitempty" binding:"omitempty,min=0" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"` + FromX float64 `json:"from_x,omitempty" yaml:"from_x,omitempty" binding:"omitempty,min=0" desc:"Starting X coordinate"` + FromY float64 `json:"from_y,omitempty" yaml:"from_y,omitempty" binding:"omitempty,min=0" desc:"Starting Y coordinate"` + ToX float64 `json:"to_x,omitempty" yaml:"to_x,omitempty" binding:"omitempty,min=0" desc:"Ending X coordinate"` + ToY float64 `json:"to_y,omitempty" yaml:"to_y,omitempty" binding:"omitempty,min=0" desc:"Ending Y coordinate"` + Text string `json:"text,omitempty" yaml:"text,omitempty" desc:"Text content for input/search operations"` + + // App/Package related + PackageName string `json:"packageName,omitempty" yaml:"packageName,omitempty" desc:"Package name of the app"` + AppName string `json:"appName,omitempty" yaml:"appName,omitempty" desc:"App name to find"` + AppUrl string `json:"appUrl,omitempty" yaml:"appUrl,omitempty" desc:"App URL for installation"` + MappingUrl string `json:"mappingUrl,omitempty" yaml:"mappingUrl,omitempty" desc:"Mapping URL for app installation"` + ResourceMappingUrl string `json:"resourceMappingUrl,omitempty" yaml:"resourceMappingUrl,omitempty" desc:"Resource mapping URL for app installation"` + + // Web/Browser related + Selector string `json:"selector,omitempty" yaml:"selector,omitempty" desc:"CSS or XPath selector"` + TabIndex int `json:"tabIndex,omitempty" yaml:"tabIndex,omitempty" desc:"Browser tab index"` + PhoneNumber string `json:"phoneNumber,omitempty" yaml:"phoneNumber,omitempty" desc:"Phone number for login"` + Captcha string `json:"captcha,omitempty" yaml:"captcha,omitempty" desc:"Captcha code"` + Password string `json:"password,omitempty" yaml:"password,omitempty" desc:"Password for login"` + + // Button/Key related + Button types.DeviceButton `json:"button,omitempty" yaml:"button,omitempty" desc:"Device button to press"` + Ime string `json:"ime,omitempty" yaml:"ime,omitempty" desc:"IME package name"` + Count int `json:"count,omitempty" yaml:"count,omitempty" desc:"Count for delete operations"` + Keycode int `json:"keycode,omitempty" yaml:"keycode,omitempty" desc:"Keycode for key press operations"` + + // Image/CV related + ImagePath string `json:"imagePath,omitempty" yaml:"imagePath,omitempty" desc:"Path to reference image for CV recognition"` + + // HTTP API specific fields + FileUrl string `json:"file_url,omitempty" yaml:"file_url,omitempty" desc:"File URL for upload operations"` + FileFormat string `json:"file_format,omitempty" yaml:"file_format,omitempty" desc:"File format for upload operations"` + ImageUrl string `json:"imageUrl,omitempty" yaml:"imageUrl,omitempty" desc:"Image URL for media operations"` + VideoUrl string `json:"videoUrl,omitempty" yaml:"videoUrl,omitempty" desc:"Video URL for media operations"` + Delta int `json:"delta,omitempty" yaml:"delta,omitempty" desc:"Delta value for scroll operations"` + Width int `json:"width,omitempty" yaml:"width,omitempty" desc:"Width for browser creation"` + Height int `json:"height,omitempty" yaml:"height,omitempty" desc:"Height for browser creation"` + + // Array parameters + Texts []string `json:"texts,omitempty" yaml:"texts,omitempty" desc:"List of texts to search"` + Params []float64 `json:"params,omitempty" yaml:"params,omitempty" desc:"Generic parameter array"` + + // AI related + Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty" desc:"AI action prompt"` + Content string `json:"content,omitempty" yaml:"content,omitempty" desc:"Content for finished action"` + + // Time related + Seconds float64 `json:"seconds,omitempty" yaml:"seconds,omitempty" desc:"Sleep duration in seconds"` + Milliseconds int64 `json:"milliseconds,omitempty" yaml:"milliseconds,omitempty" desc:"Sleep duration in milliseconds"` + + // Control options + Context context.Context `json:"-" yaml:"-"` + Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty" desc:"Action identifier for logging"` + MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty" desc:"Maximum retry times"` + Interval float64 `json:"interval,omitempty" yaml:"interval,omitempty" desc:"Interval between retries in seconds"` + Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty" desc:"Action duration in seconds"` + PressDuration float64 `json:"press_duration,omitempty" yaml:"press_duration,omitempty" desc:"Press duration in seconds"` + Steps int `json:"steps,omitempty" yaml:"steps,omitempty" desc:"Number of steps for action"` + Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty" desc:"Direction for swipe operations or custom coordinates"` + Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty" desc:"Timeout in seconds"` + Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty" desc:"Action frequency"` ScreenOptions - // set custiom options such as textview, id, description - Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"` + // Custom options + Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty" desc:"Custom options"` } func (o *ActionOptions) Options() []ActionOption { @@ -433,3 +493,308 @@ func WithIgnoreNotFoundError(ignoreError bool) ActionOption { o.IgnoreNotFoundError = ignoreError } } + +// HTTP API direct usage methods + +// ValidateForHTTPAPI validates the request for HTTP API usage +func (o *ActionOptions) ValidateForHTTPAPI(actionType ActionName) error { + // Basic validation - Platform and Serial are set from URL, so skip here + // They will be validated by setRequestContextFromURL + + // Action-specific validation using a more efficient approach + return o.validateActionSpecificFields(actionType) +} + +// validateActionSpecificFields performs action-specific field validation +func (o *ActionOptions) validateActionSpecificFields(actionType ActionName) error { + // Define validation rules for each action type using ActionMethod constants + validationRules := map[ActionName]func() error{ + ACTION_Tap: func() error { + return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) + }, + ACTION_TapXY: func() error { + return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) + }, + ACTION_TapAbsXY: func() error { + return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) + }, + ACTION_DoubleTap: func() error { + return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) + }, + ACTION_DoubleTapXY: func() error { + return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) + }, + ACTION_RightClick: func() error { + return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) + }, + ACTION_SecondaryClick: func() error { + return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) + }, + ACTION_Hover: func() error { + return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) + }, + ACTION_Drag: func() error { + return o.requireFields("fromX, fromY, toX, toY coordinates", + o.FromX != 0 && o.FromY != 0 && o.ToX != 0 && o.ToY != 0) + }, + ACTION_SwipeCoordinate: func() error { + return o.requireFields("fromX, fromY, toX, toY coordinates", + o.FromX != 0 && o.FromY != 0 && o.ToX != 0 && o.ToY != 0) + }, + ACTION_Swipe: func() error { + return o.requireFields("direction", o.Direction != nil && o.Direction != "") + }, + ACTION_SwipeDirection: func() error { + return o.requireFields("direction", o.Direction != nil && o.Direction != "") + }, + ACTION_Input: func() error { + return o.requireFields("text", o.Text != "") + }, + ACTION_Delete: func() error { + // Count is optional, will use default if not provided + return nil + }, + ACTION_Backspace: func() error { + // Count is optional, will use default if not provided + return nil + }, + ACTION_KeyCode: func() error { + return o.requireFields("keycode", o.Keycode != 0) + }, + ACTION_Scroll: func() error { + return o.requireFields("delta", o.Delta != 0) + }, + ACTION_AppInfo: func() error { + return o.requireFields("packageName", o.PackageName != "") + }, + ACTION_AppClear: func() error { + return o.requireFields("packageName", o.PackageName != "") + }, + ACTION_AppLaunch: func() error { + return o.requireFields("packageName", o.PackageName != "") + }, + ACTION_AppTerminate: func() error { + return o.requireFields("packageName", o.PackageName != "") + }, + ACTION_AppUninstall: func() error { + return o.requireFields("packageName", o.PackageName != "") + }, + ACTION_AppInstall: func() error { + return o.requireFields("appUrl", o.AppUrl != "") + }, + ACTION_TapByOCR: func() error { + return o.requireFields("text", o.Text != "") + }, + ACTION_SwipeToTapText: func() error { + return o.requireFields("text", o.Text != "") + }, + ACTION_TapByCV: func() error { + return o.requireFields("imagePath", o.ImagePath != "") + }, + ACTION_SwipeToTapApp: func() error { + return o.requireFields("appName", o.AppName != "") + }, + ACTION_SwipeToTapTexts: func() error { + return o.requireFields("texts array", len(o.Texts) > 0) + }, + ACTION_TapBySelector: func() error { + return o.requireFields("selector", o.Selector != "") + }, + ACTION_HoverBySelector: func() error { + return o.requireFields("selector", o.Selector != "") + }, + ACTION_SecondaryClickBySelector: func() error { + return o.requireFields("selector", o.Selector != "") + }, + ACTION_WebCloseTab: func() error { + return o.requireFields("tabIndex", o.TabIndex != 0) + }, + ACTION_WebLoginNoneUI: func() error { + if o.PackageName == "" || o.PhoneNumber == "" || o.Captcha == "" || o.Password == "" { + return fmt.Errorf("packageName, phoneNumber, captcha, and password are required for web_login_none_ui action") + } + return nil + }, + ACTION_SetIme: func() error { + return o.requireFields("ime", o.Ime != "") + }, + ACTION_GetSource: func() error { + return o.requireFields("packageName", o.PackageName != "") + }, + ACTION_SleepMS: func() error { + return o.requireFields("milliseconds", o.Milliseconds != 0) + }, + ACTION_SleepRandom: func() error { + return o.requireFields("params array", len(o.Params) > 0) + }, + ACTION_AIAction: func() error { + return o.requireFields("prompt", o.Prompt != "") + }, + ACTION_Finished: func() error { + return o.requireFields("content", o.Content != "") + }, + ACTION_Upload: func() error { + if o.X == 0 || o.Y == 0 || o.FileUrl == "" { + return fmt.Errorf("x, y coordinates and fileUrl are required for upload action") + } + return nil + }, + ACTION_PushMedia: func() error { + if o.ImageUrl == "" && o.VideoUrl == "" { + return fmt.Errorf("either imageUrl or videoUrl is required for push_media action") + } + return nil + }, + ACTION_CreateBrowser: func() error { + return o.requireFields("timeout", o.Timeout != 0) + }, + } + + // Execute validation rule for the action type + if validator, exists := validationRules[actionType]; exists { + return validator() + } + + // No specific validation needed for this action type + return nil +} + +// requireFields is a helper function to generate consistent error messages +func (o *ActionOptions) requireFields(fieldDesc string, condition bool) error { + if !condition { + return fmt.Errorf("%s is required for this action", fieldDesc) + } + return nil +} + +// GetMCPOptions generates MCP tool options for specific action types +func (o *ActionOptions) GetMCPOptions(actionType ActionName) []mcp.ToolOption { + // Define field mappings for different action types + fieldMappings := map[ActionName][]string{ + ACTION_TapXY: {"platform", "serial", "x", "y", "duration"}, + ACTION_TapAbsXY: {"platform", "serial", "x", "y", "duration"}, + ACTION_TapByOCR: {"platform", "serial", "text", "ignoreNotFoundError", "maxRetryTimes", "index", "regex", "tapRandomRect"}, + ACTION_TapByCV: {"platform", "serial", "ignoreNotFoundError", "maxRetryTimes", "index", "tapRandomRect"}, + ACTION_DoubleTapXY: {"platform", "serial", "x", "y"}, + ACTION_SwipeDirection: {"platform", "serial", "direction", "duration", "pressDuration"}, + ACTION_SwipeCoordinate: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"}, + ACTION_Swipe: {"platform", "serial", "direction", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"}, + ACTION_Drag: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"}, + ACTION_Input: {"platform", "serial", "text", "frequency"}, + ACTION_AppLaunch: {"platform", "serial", "packageName"}, + ACTION_AppTerminate: {"platform", "serial", "packageName"}, + ACTION_AppInstall: {"platform", "serial", "appUrl", "packageName"}, + ACTION_AppUninstall: {"platform", "serial", "packageName"}, + ACTION_AppClear: {"platform", "serial", "packageName"}, + ACTION_PressButton: {"platform", "serial", "button"}, + ACTION_SwipeToTapApp: {"platform", "serial", "appName", "ignoreNotFoundError", "maxRetryTimes", "index"}, + ACTION_SwipeToTapText: {"platform", "serial", "text", "ignoreNotFoundError", "maxRetryTimes", "index", "regex"}, + ACTION_SwipeToTapTexts: {"platform", "serial", "texts", "ignoreNotFoundError", "maxRetryTimes", "index", "regex"}, + ACTION_SecondaryClick: {"platform", "serial", "x", "y"}, + ACTION_HoverBySelector: {"platform", "serial", "selector"}, + ACTION_TapBySelector: {"platform", "serial", "selector"}, + ACTION_SecondaryClickBySelector: {"platform", "serial", "selector"}, + ACTION_WebCloseTab: {"platform", "serial", "tabIndex"}, + ACTION_WebLoginNoneUI: {"platform", "serial", "packageName", "phoneNumber", "captcha", "password"}, + ACTION_SetIme: {"platform", "serial", "ime"}, + ACTION_GetSource: {"platform", "serial", "packageName"}, + ACTION_Sleep: {"seconds"}, + ACTION_SleepMS: {"platform", "serial", "milliseconds"}, + ACTION_SleepRandom: {"platform", "serial", "params"}, + ACTION_AIAction: {"platform", "serial", "prompt"}, + ACTION_Finished: {"content"}, + ACTION_ListAvailableDevices: {}, + ACTION_SelectDevice: {"platform", "serial"}, + ACTION_ScreenShot: {"platform", "serial"}, + ACTION_GetScreenSize: {"platform", "serial"}, + ACTION_Home: {"platform", "serial"}, + ACTION_Back: {"platform", "serial"}, + ACTION_ListPackages: {"platform", "serial"}, + ACTION_ClosePopups: {"platform", "serial"}, + } + + fields := fieldMappings[actionType] + // Generate options for specified fields, or all fields if not mapped + return o.generateMCPOptionsForFields(fields) +} + +// generateMCPOptionsForFields generates MCP options for specific fields +func (o *ActionOptions) generateMCPOptionsForFields(fields []string) []mcp.ToolOption { + options := make([]mcp.ToolOption, 0) + + // If no fields are specified, return empty options (e.g., for ACTION_ListAvailableDevices) + if len(fields) == 0 { + return options + } + + rType := reflect.TypeOf(*o) + + // Process specific fields + fieldMap := make(map[string]reflect.StructField) + for i := 0; i < rType.NumField(); i++ { + field := rType.Field(i) + jsonTag := field.Tag.Get("json") + if jsonTag != "" && jsonTag != "-" { + name := strings.Split(jsonTag, ",")[0] + fieldMap[name] = field + } + } + + for _, fieldName := range fields { + field, exists := fieldMap[fieldName] + if !exists { + continue + } + + jsonTag := field.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + name := strings.Split(jsonTag, ",")[0] + binding := field.Tag.Get("binding") + required := strings.Contains(binding, "required") + desc := field.Tag.Get("desc") + + // Handle pointer types + fieldType := field.Type + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + switch fieldType.Kind() { + case reflect.Float64, reflect.Float32, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if required { + options = append(options, mcp.WithNumber(name, mcp.Required(), mcp.Description(desc))) + } else { + options = append(options, mcp.WithNumber(name, mcp.Description(desc))) + } + case reflect.String: + if required { + options = append(options, mcp.WithString(name, mcp.Required(), mcp.Description(desc))) + } else { + options = append(options, mcp.WithString(name, mcp.Description(desc))) + } + case reflect.Bool: + if required { + options = append(options, mcp.WithBoolean(name, mcp.Required(), mcp.Description(desc))) + } else { + options = append(options, mcp.WithBoolean(name, mcp.Description(desc))) + } + case reflect.Slice: + if fieldType.Elem().Kind() == reflect.String || fieldType.Elem().Kind() == reflect.Float64 { + if required { + options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc))) + } else { + options = append(options, mcp.WithArray(name, mcp.Description(desc))) + } + } + case reflect.Map, reflect.Interface: + // Skip map and interface types for now + continue + default: + log.Warn().Str("field_type", fieldType.String()).Msg("Unsupported field type") + } + } + + return options +} diff --git a/uixt/option/action_test.go b/uixt/option/action_test.go new file mode 100644 index 00000000..9d7bb2e6 --- /dev/null +++ b/uixt/option/action_test.go @@ -0,0 +1,175 @@ +package option + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnifiedActionRequest_Options(t *testing.T) { + // Test TapXY request conversion + unifiedReq := &ActionOptions{ + Platform: "android", + Serial: "device123", + X: 0.5, + Y: 0.7, + Duration: 1.0, + MaxRetryTimes: 3, + ScreenOptions: ScreenOptions{ + ScreenFilterOptions: ScreenFilterOptions{ + Regex: true, + }, + }, + } + + actionOpts := unifiedReq.Options() + + assert.Equal(t, 1.0, unifiedReq.Duration) + assert.Equal(t, 3, unifiedReq.MaxRetryTimes) + assert.True(t, unifiedReq.Regex) + assert.NotEmpty(t, actionOpts) +} + +func TestUnifiedActionRequest_GetMCPOptions(t *testing.T) { + unifiedReq := &ActionOptions{ + Platform: "android", + Serial: "device123", + } + + // Test TapXY options + tapOptions := unifiedReq.GetMCPOptions(ACTION_TapXY) + assert.NotEmpty(t, tapOptions) + + // Test TapByOCR options + ocrOptions := unifiedReq.GetMCPOptions(ACTION_TapByOCR) + assert.NotEmpty(t, ocrOptions) + + // Test unknown action (should return empty options) + unknownOptions := unifiedReq.GetMCPOptions("unknown_action") + assert.Empty(t, unknownOptions) +} + +func TestUnifiedActionRequest_SwipeDirection(t *testing.T) { + unifiedReq := &ActionOptions{ + Platform: "android", + Serial: "device123", + Direction: "up", + Duration: 2.0, + PressDuration: 0.5, + } + + opts := unifiedReq.Options() + assert.Equal(t, "up", unifiedReq.Direction) + assert.Equal(t, 2.0, unifiedReq.Duration) + assert.Equal(t, 0.5, unifiedReq.PressDuration) + assert.NotEmpty(t, opts) +} + +func TestUnifiedActionRequest_SwipeCoordinate(t *testing.T) { + params := []float64{0.2, 0.8, 0.2, 0.2} + + unifiedReq := &ActionOptions{ + Platform: "android", + Serial: "device123", + Direction: params, + } + + opts := unifiedReq.Options() + assert.Equal(t, params, unifiedReq.Direction) + assert.NotEmpty(t, opts) +} + +func TestUnifiedActionRequest_ScreenOptions(t *testing.T) { + uiTypes := []string{"button", "text"} + + unifiedReq := &ActionOptions{ + Platform: "android", + Serial: "device123", + ScreenOptions: ScreenOptions{ + ScreenShotOptions: ScreenShotOptions{ + ScreenShotWithOCR: true, + ScreenShotWithUpload: true, + ScreenShotWithUITypes: uiTypes, + }, + }, + } + + opts := unifiedReq.Options() + assert.True(t, unifiedReq.ScreenShotWithOCR) + assert.True(t, unifiedReq.ScreenShotWithUpload) + assert.Equal(t, uiTypes, unifiedReq.ScreenShotWithUITypes) + assert.NotEmpty(t, opts) +} + +func TestUnifiedActionRequest_NilPointerSafety(t *testing.T) { + // Test with nil pointers + unifiedReq := &ActionOptions{ + Platform: "android", + Serial: "device123", + // All pointer fields are nil + } + + opts := unifiedReq.Options() + assert.Equal(t, 0, unifiedReq.MaxRetryTimes) + assert.Equal(t, 0.0, unifiedReq.Duration) + assert.Equal(t, 0.0, unifiedReq.PressDuration) + assert.False(t, unifiedReq.Regex) + assert.False(t, unifiedReq.TapRandomRect) + // When all fields are default values, Options() may return empty slice + // This is expected behavior + assert.NotNil(t, opts) +} + +func TestUnifiedActionRequest_CustomOptions(t *testing.T) { + customData := map[string]interface{}{ + "custom_key": "custom_value", + "number": 42, + } + + unifiedReq := &ActionOptions{ + Platform: "android", + Serial: "device123", + Custom: customData, + } + + opts := unifiedReq.Options() + assert.Equal(t, customData, unifiedReq.Custom) + assert.NotEmpty(t, opts) +} + +func TestUnifiedActionRequest_BasicTypeFields(t *testing.T) { + // Test basic type fields (no longer pointers) + unifiedReq := &ActionOptions{ + Platform: "android", + Serial: "device123", + Count: 5, + Keycode: 123, + Delta: 10, + Width: 800, + Height: 600, + Seconds: 2.5, + Milliseconds: 1500, + TabIndex: 3, + } + + // Test direct field access (no need for Getter methods) + assert.Equal(t, 5, unifiedReq.Count) + assert.Equal(t, 123, unifiedReq.Keycode) + assert.Equal(t, 10, unifiedReq.Delta) + assert.Equal(t, 800, unifiedReq.Width) + assert.Equal(t, 600, unifiedReq.Height) + assert.Equal(t, 2.5, unifiedReq.Seconds) + assert.Equal(t, int64(1500), unifiedReq.Milliseconds) + assert.Equal(t, 3, unifiedReq.TabIndex) + + // Test zero value detection + emptyReq := &ActionOptions{} + assert.Equal(t, 0, emptyReq.Count) + assert.Equal(t, 0, emptyReq.Keycode) + assert.Equal(t, 0, emptyReq.Delta) + assert.Equal(t, 0, emptyReq.Width) + assert.Equal(t, 0, emptyReq.Height) + assert.Equal(t, 0.0, emptyReq.Seconds) + assert.Equal(t, int64(0), emptyReq.Milliseconds) + assert.Equal(t, 0, emptyReq.TabIndex) +} diff --git a/uixt/option/request.go b/uixt/option/request.go deleted file mode 100644 index ae8011d6..00000000 --- a/uixt/option/request.go +++ /dev/null @@ -1,752 +0,0 @@ -package option - -import ( - "context" - "fmt" - "reflect" - "strings" - - "github.com/httprunner/httprunner/v5/uixt/types" - "github.com/mark3labs/mcp-go/mcp" - "github.com/rs/zerolog/log" -) - -// NewMCPOptions creates MCP tool options from a struct using reflection -// This function is kept for backward compatibility with existing code -// New code should use UnifiedActionRequest.GetMCPOptions() instead -func NewMCPOptions(t interface{}) (options []mcp.ToolOption) { - tType := reflect.TypeOf(t) - - // Handle pointer type by getting the element type - if tType.Kind() == reflect.Ptr { - tType = tType.Elem() - } - - // Ensure we have a struct type - if tType.Kind() != reflect.Struct { - log.Warn().Str("type", tType.String()).Msg("NewMCPOptions expects a struct or pointer to struct") - return options - } - - for i := 0; i < tType.NumField(); i++ { - field := tType.Field(i) - jsonTag := field.Tag.Get("json") - if jsonTag == "" || jsonTag == "-" { - continue - } - name := strings.Split(jsonTag, ",")[0] - binding := field.Tag.Get("binding") - required := strings.Contains(binding, "required") - desc := field.Tag.Get("desc") - fieldType := field.Type - // Handle pointer types - if fieldType.Kind() == reflect.Ptr { - fieldType = fieldType.Elem() - } - - switch fieldType.Kind() { - case reflect.Float64, reflect.Float32, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if required { - options = append(options, mcp.WithNumber(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithNumber(name, mcp.Description(desc))) - } - case reflect.String: - if required { - options = append(options, mcp.WithString(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithString(name, mcp.Description(desc))) - } - case reflect.Bool: - if required { - options = append(options, mcp.WithBoolean(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithBoolean(name, mcp.Description(desc))) - } - case reflect.Slice: - // Handle slice types, especially []string and []float64 - if field.Type.Elem().Kind() == reflect.String { - // Array of strings - if required { - options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithArray(name, mcp.Description(desc))) - } - } else if field.Type.Elem().Kind() == reflect.Float64 { - // Array of numbers - if required { - options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithArray(name, mcp.Description(desc))) - } - } - default: - log.Warn().Str("field_type", field.Type.String()).Msg("Unsupported field type") - } - } - return options -} - -// UnifiedActionRequest represents a unified request structure that combines -// ActionOptions with specific action parameters -type UnifiedActionRequest struct { - // Device targeting - Platform string `json:"platform" binding:"omitempty" desc:"Device platform: android/ios/browser"` - Serial string `json:"serial" binding:"omitempty" desc:"Device serial/udid/browser id"` - - // Common action parameters - X *float64 `json:"x,omitempty" binding:"omitempty,min=0" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"` - Y *float64 `json:"y,omitempty" binding:"omitempty,min=0" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"` - FromX *float64 `json:"from_x,omitempty" binding:"omitempty,min=0" desc:"Starting X coordinate"` - FromY *float64 `json:"from_y,omitempty" binding:"omitempty,min=0" desc:"Starting Y coordinate"` - ToX *float64 `json:"to_x,omitempty" binding:"omitempty,min=0" desc:"Ending X coordinate"` - ToY *float64 `json:"to_y,omitempty" binding:"omitempty,min=0" desc:"Ending Y coordinate"` - Text string `json:"text,omitempty" desc:"Text content for input/search operations"` - Direction string `json:"direction,omitempty" desc:"Direction for swipe operations: up/down/left/right"` - - // App/Package related - PackageName string `json:"packageName,omitempty" desc:"Package name of the app"` - AppName string `json:"appName,omitempty" desc:"App name to find"` - AppUrl string `json:"appUrl,omitempty" desc:"App URL for installation"` - MappingUrl string `json:"mappingUrl,omitempty" desc:"Mapping URL for app installation"` - ResourceMappingUrl string `json:"resourceMappingUrl,omitempty" desc:"Resource mapping URL for app installation"` - - // Web/Browser related - Selector string `json:"selector,omitempty" desc:"CSS or XPath selector"` - TabIndex *int `json:"tabIndex,omitempty" desc:"Browser tab index"` - PhoneNumber string `json:"phoneNumber,omitempty" desc:"Phone number for login"` - Captcha string `json:"captcha,omitempty" desc:"Captcha code"` - Password string `json:"password,omitempty" desc:"Password for login"` - - // Button/Key related - Button types.DeviceButton `json:"button,omitempty" desc:"Device button to press"` - Ime string `json:"ime,omitempty" desc:"IME package name"` - Count *int `json:"count,omitempty" desc:"Count for delete operations"` - Keycode *int `json:"keycode,omitempty" desc:"Keycode for key press operations"` - - // Image/CV related - ImagePath string `json:"imagePath,omitempty" desc:"Path to reference image for CV recognition"` - - // HTTP API specific fields - FileUrl string `json:"file_url,omitempty" desc:"File URL for upload operations"` - FileFormat string `json:"file_format,omitempty" desc:"File format for upload operations"` - ImageUrl string `json:"imageUrl,omitempty" desc:"Image URL for media operations"` - VideoUrl string `json:"videoUrl,omitempty" desc:"Video URL for media operations"` - Delta *int `json:"delta,omitempty" desc:"Delta value for scroll operations"` - Width *int `json:"width,omitempty" desc:"Width for browser creation"` - Height *int `json:"height,omitempty" desc:"Height for browser creation"` - - // Array parameters - Texts []string `json:"texts,omitempty" desc:"List of texts to search"` - Params []float64 `json:"params,omitempty" desc:"Generic parameter array"` - - // AI related - Prompt string `json:"prompt,omitempty" desc:"AI action prompt"` - Content string `json:"content,omitempty" desc:"Content for finished action"` - - // Time related - Seconds *float64 `json:"seconds,omitempty" desc:"Sleep duration in seconds"` - Milliseconds *int64 `json:"milliseconds,omitempty" desc:"Sleep duration in milliseconds"` - - // Control options (from ActionOptions) - Context context.Context `json:"-" yaml:"-"` - Identifier string `json:"identifier,omitempty" desc:"Action identifier for logging"` - MaxRetryTimes *int `json:"max_retry_times,omitempty" desc:"Maximum retry times"` - Interval *float64 `json:"interval,omitempty" desc:"Interval between retries in seconds"` - Duration *float64 `json:"duration,omitempty" desc:"Action duration in seconds"` - PressDuration *float64 `json:"press_duration,omitempty" desc:"Press duration in seconds"` - Steps *int `json:"steps,omitempty" desc:"Number of steps for action"` - Timeout *int `json:"timeout,omitempty" desc:"Timeout in seconds"` - Frequency *int `json:"frequency,omitempty" desc:"Action frequency"` - - // Filter options (from ScreenFilterOptions) - Scope []float64 `json:"scope,omitempty" desc:"Screen scope [x1,y1,x2,y2] in percentage"` - AbsScope []int `json:"absScope,omitempty" desc:"Absolute screen scope [x1,y1,x2,y2] in pixels"` - Regex *bool `json:"regex,omitempty" desc:"Use regex to match text"` - TapOffset []int `json:"tap_offset,omitempty" desc:"Tap offset [x,y]"` - TapRandomRect *bool `json:"tap_random_rect,omitempty" desc:"Tap random point in rectangle"` - SwipeOffset []int `json:"swipe_offset,omitempty" desc:"Swipe offset [fromX,fromY,toX,toY]"` - OffsetRandomRange []int `json:"offset_random_range,omitempty" desc:"Random offset range [min,max]"` - Index *int `json:"index,omitempty" desc:"Element index when multiple matches found"` - MatchOne *bool `json:"match_one,omitempty" desc:"Match only one element"` - IgnoreNotFoundError *bool `json:"ignore_NotFoundError,omitempty" desc:"Ignore error if element not found"` - - // Screenshot options (from ScreenShotOptions) - ScreenShotWithOCR *bool `json:"screenshot_with_ocr,omitempty" desc:"Take screenshot with OCR"` - ScreenShotWithUpload *bool `json:"screenshot_with_upload,omitempty" desc:"Upload screenshot"` - ScreenShotWithLiveType *bool `json:"screenshot_with_live_type,omitempty" desc:"Screenshot with live type"` - ScreenShotWithLivePopularity *bool `json:"screenshot_with_live_popularity,omitempty" desc:"Screenshot with live popularity"` - ScreenShotWithUITypes []string `json:"screenshot_with_ui_types,omitempty" desc:"Screenshot with UI types"` - ScreenShotWithClosePopups *bool `json:"screenshot_with_close_popups,omitempty" desc:"Close popups before screenshot"` - ScreenShotWithOCRCluster string `json:"screenshot_with_ocr_cluster,omitempty" desc:"OCR cluster for screenshot"` - ScreenShotFileName string `json:"screenshot_file_name,omitempty" desc:"Screenshot file name"` - - // Screen record options (from ScreenRecordOptions) - ScreenRecordDuration *float64 `json:"screenrecord_duration,omitempty" desc:"Screen record duration"` - ScreenRecordWithAudio *bool `json:"screenrecord_with_audio,omitempty" desc:"Record with audio"` - ScreenRecordWithScrcpy *bool `json:"screenrecord_with_scrcpy,omitempty" desc:"Use scrcpy for recording"` - ScreenRecordPath string `json:"screenrecord_path,omitempty" desc:"Screen record output path"` - - // Mark operation options (from MarkOperationOptions) - PreMarkOperation *bool `json:"pre_mark_operation,omitempty" desc:"Mark operation before action"` - PostMarkOperation *bool `json:"post_mark_operation,omitempty" desc:"Mark operation after action"` - - // Custom options - Custom map[string]interface{} `json:"custom,omitempty" desc:"Custom options"` -} - -// HTTP API direct usage methods - -// GetX returns the X coordinate value, handling nil pointer safely -func (r *UnifiedActionRequest) GetX() float64 { - if r.X != nil { - return *r.X - } - return 0 -} - -// GetY returns the Y coordinate value, handling nil pointer safely -func (r *UnifiedActionRequest) GetY() float64 { - if r.Y != nil { - return *r.Y - } - return 0 -} - -// GetFromX returns the FromX coordinate value, handling nil pointer safely -func (r *UnifiedActionRequest) GetFromX() float64 { - if r.FromX != nil { - return *r.FromX - } - return 0 -} - -// GetFromY returns the FromY coordinate value, handling nil pointer safely -func (r *UnifiedActionRequest) GetFromY() float64 { - if r.FromY != nil { - return *r.FromY - } - return 0 -} - -// GetToX returns the ToX coordinate value, handling nil pointer safely -func (r *UnifiedActionRequest) GetToX() float64 { - if r.ToX != nil { - return *r.ToX - } - return 0 -} - -// GetToY returns the ToY coordinate value, handling nil pointer safely -func (r *UnifiedActionRequest) GetToY() float64 { - if r.ToY != nil { - return *r.ToY - } - return 0 -} - -// GetDuration returns the duration value, handling nil pointer safely -func (r *UnifiedActionRequest) GetDuration() float64 { - if r.Duration != nil { - return *r.Duration - } - return 0 -} - -// GetPressDuration returns the press duration value, handling nil pointer safely -func (r *UnifiedActionRequest) GetPressDuration() float64 { - if r.PressDuration != nil { - return *r.PressDuration - } - return 0 -} - -// GetCount returns the count value, handling nil pointer safely -func (r *UnifiedActionRequest) GetCount() int { - if r.Count != nil { - return *r.Count - } - return 0 -} - -// GetKeycode returns the keycode value, handling nil pointer safely -func (r *UnifiedActionRequest) GetKeycode() int { - if r.Keycode != nil { - return *r.Keycode - } - return 0 -} - -// GetFrequency returns the frequency value, handling nil pointer safely -func (r *UnifiedActionRequest) GetFrequency() int { - if r.Frequency != nil { - return *r.Frequency - } - return 0 -} - -// GetTabIndex returns the tab index value, handling nil pointer safely -func (r *UnifiedActionRequest) GetTabIndex() int { - if r.TabIndex != nil { - return *r.TabIndex - } - return 0 -} - -// GetDelta returns the delta value, handling nil pointer safely -func (r *UnifiedActionRequest) GetDelta() int { - if r.Delta != nil { - return *r.Delta - } - return 0 -} - -// GetWidth returns the width value, handling nil pointer safely -func (r *UnifiedActionRequest) GetWidth() int { - if r.Width != nil { - return *r.Width - } - return 0 -} - -// GetHeight returns the height value, handling nil pointer safely -func (r *UnifiedActionRequest) GetHeight() int { - if r.Height != nil { - return *r.Height - } - return 0 -} - -// GetTimeout returns the timeout value, handling nil pointer safely -func (r *UnifiedActionRequest) GetTimeout() int { - if r.Timeout != nil { - return *r.Timeout - } - return 0 -} - -// GetMilliseconds returns the milliseconds value, handling nil pointer safely -func (r *UnifiedActionRequest) GetMilliseconds() int64 { - if r.Milliseconds != nil { - return *r.Milliseconds - } - return 0 -} - -// ValidateForHTTPAPI validates the request for HTTP API usage -func (r *UnifiedActionRequest) ValidateForHTTPAPI(actionType ActionMethod) error { - // Basic validation - Platform and Serial are set from URL, so skip here - // They will be validated by setRequestContextFromURL - - // Action-specific validation using a more efficient approach - return r.validateActionSpecificFields(actionType) -} - -// validateActionSpecificFields performs action-specific field validation -func (r *UnifiedActionRequest) validateActionSpecificFields(actionType ActionMethod) error { - // Define validation rules for each action type using ActionMethod constants - validationRules := map[ActionMethod]func() error{ - ACTION_Tap: func() error { - return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil) - }, - ACTION_TapXY: func() error { - return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil) - }, - ACTION_TapAbsXY: func() error { - return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil) - }, - ACTION_DoubleTap: func() error { - return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil) - }, - ACTION_DoubleTapXY: func() error { - return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil) - }, - ACTION_RightClick: func() error { - return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil) - }, - ACTION_SecondaryClick: func() error { - return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil) - }, - ACTION_Hover: func() error { - return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil) - }, - ACTION_Drag: func() error { - return r.requireFields("fromX, fromY, toX, toY coordinates", - r.FromX != nil && r.FromY != nil && r.ToX != nil && r.ToY != nil) - }, - ACTION_SwipeCoordinate: func() error { - return r.requireFields("fromX, fromY, toX, toY coordinates", - r.FromX != nil && r.FromY != nil && r.ToX != nil && r.ToY != nil) - }, - ACTION_Swipe: func() error { - return r.requireFields("direction", r.Direction != "") - }, - ACTION_SwipeDirection: func() error { - return r.requireFields("direction", r.Direction != "") - }, - ACTION_Input: func() error { - return r.requireFields("text", r.Text != "") - }, - ACTION_Delete: func() error { - // Count is optional, will use default if not provided - return nil - }, - ACTION_Backspace: func() error { - // Count is optional, will use default if not provided - return nil - }, - ACTION_KeyCode: func() error { - return r.requireFields("keycode", r.Keycode != nil) - }, - ACTION_Scroll: func() error { - return r.requireFields("delta", r.Delta != nil) - }, - ACTION_AppInfo: func() error { - return r.requireFields("packageName", r.PackageName != "") - }, - ACTION_AppClear: func() error { - return r.requireFields("packageName", r.PackageName != "") - }, - ACTION_AppLaunch: func() error { - return r.requireFields("packageName", r.PackageName != "") - }, - ACTION_AppTerminate: func() error { - return r.requireFields("packageName", r.PackageName != "") - }, - ACTION_AppUninstall: func() error { - return r.requireFields("packageName", r.PackageName != "") - }, - ACTION_AppInstall: func() error { - return r.requireFields("appUrl", r.AppUrl != "") - }, - ACTION_TapByOCR: func() error { - return r.requireFields("text", r.Text != "") - }, - ACTION_SwipeToTapText: func() error { - return r.requireFields("text", r.Text != "") - }, - ACTION_TapByCV: func() error { - return r.requireFields("imagePath", r.ImagePath != "") - }, - ACTION_SwipeToTapApp: func() error { - return r.requireFields("appName", r.AppName != "") - }, - ACTION_SwipeToTapTexts: func() error { - return r.requireFields("texts array", len(r.Texts) > 0) - }, - ACTION_TapBySelector: func() error { - return r.requireFields("selector", r.Selector != "") - }, - ACTION_HoverBySelector: func() error { - return r.requireFields("selector", r.Selector != "") - }, - ACTION_SecondaryClickBySelector: func() error { - return r.requireFields("selector", r.Selector != "") - }, - ACTION_WebCloseTab: func() error { - return r.requireFields("tabIndex", r.TabIndex != nil) - }, - ACTION_WebLoginNoneUI: func() error { - if r.PackageName == "" || r.PhoneNumber == "" || r.Captcha == "" || r.Password == "" { - return fmt.Errorf("packageName, phoneNumber, captcha, and password are required for web_login_none_ui action") - } - return nil - }, - ACTION_SetIme: func() error { - return r.requireFields("ime", r.Ime != "") - }, - ACTION_GetSource: func() error { - return r.requireFields("packageName", r.PackageName != "") - }, - ACTION_SleepMS: func() error { - return r.requireFields("milliseconds", r.Milliseconds != nil) - }, - ACTION_SleepRandom: func() error { - return r.requireFields("params array", len(r.Params) > 0) - }, - ACTION_AIAction: func() error { - return r.requireFields("prompt", r.Prompt != "") - }, - ACTION_Finished: func() error { - return r.requireFields("content", r.Content != "") - }, - ACTION_Upload: func() error { - if r.X == nil || r.Y == nil || r.FileUrl == "" { - return fmt.Errorf("x, y coordinates and fileUrl are required for upload action") - } - return nil - }, - ACTION_PushMedia: func() error { - if r.ImageUrl == "" && r.VideoUrl == "" { - return fmt.Errorf("either imageUrl or videoUrl is required for push_media action") - } - return nil - }, - ACTION_CreateBrowser: func() error { - return r.requireFields("timeout", r.Timeout != nil) - }, - } - - // Execute validation rule for the action type - if validator, exists := validationRules[actionType]; exists { - return validator() - } - - // No specific validation needed for this action type - return nil -} - -// requireFields is a helper function to generate consistent error messages -func (r *UnifiedActionRequest) requireFields(fieldDesc string, condition bool) error { - if !condition { - return fmt.Errorf("%s is required for this action", fieldDesc) - } - return nil -} - -// ToActionOptions converts UnifiedActionRequest to ActionOptions -func (r *UnifiedActionRequest) ToActionOptions() *ActionOptions { - opts := &ActionOptions{ - Context: r.Context, - Identifier: r.Identifier, - Custom: r.Custom, - } - - // Copy pointer values safely - if r.MaxRetryTimes != nil { - opts.MaxRetryTimes = *r.MaxRetryTimes - } - if r.Interval != nil { - opts.Interval = *r.Interval - } - if r.Duration != nil { - opts.Duration = *r.Duration - } - if r.PressDuration != nil { - opts.PressDuration = *r.PressDuration - } - if r.Steps != nil { - opts.Steps = *r.Steps - } - if r.Timeout != nil { - opts.Timeout = *r.Timeout - } - if r.Frequency != nil { - opts.Frequency = *r.Frequency - } - - // Handle direction - if r.Direction != "" { - opts.Direction = r.Direction - } else if len(r.Params) == 4 { - opts.Direction = r.Params - } - - // Copy filter options (ScreenFilterOptions) - opts.ScreenFilterOptions.Scope = r.Scope - opts.ScreenFilterOptions.AbsScope = r.AbsScope - if r.Regex != nil { - opts.ScreenFilterOptions.Regex = *r.Regex - } - opts.ScreenFilterOptions.TapOffset = r.TapOffset - if r.TapRandomRect != nil { - opts.ScreenFilterOptions.TapRandomRect = *r.TapRandomRect - } - opts.ScreenFilterOptions.SwipeOffset = r.SwipeOffset - opts.ScreenFilterOptions.OffsetRandomRange = r.OffsetRandomRange - if r.Index != nil { - opts.ScreenFilterOptions.Index = *r.Index - } - if r.MatchOne != nil { - opts.ScreenFilterOptions.MatchOne = *r.MatchOne - } - if r.IgnoreNotFoundError != nil { - opts.ScreenFilterOptions.IgnoreNotFoundError = *r.IgnoreNotFoundError - } - - // Copy screenshot options (ScreenShotOptions) - if r.ScreenShotWithOCR != nil { - opts.ScreenShotOptions.ScreenShotWithOCR = *r.ScreenShotWithOCR - } - if r.ScreenShotWithUpload != nil { - opts.ScreenShotOptions.ScreenShotWithUpload = *r.ScreenShotWithUpload - } - if r.ScreenShotWithLiveType != nil { - opts.ScreenShotOptions.ScreenShotWithLiveType = *r.ScreenShotWithLiveType - } - if r.ScreenShotWithLivePopularity != nil { - opts.ScreenShotOptions.ScreenShotWithLivePopularity = *r.ScreenShotWithLivePopularity - } - opts.ScreenShotOptions.ScreenShotWithUITypes = r.ScreenShotWithUITypes - if r.ScreenShotWithClosePopups != nil { - opts.ScreenShotOptions.ScreenShotWithClosePopups = *r.ScreenShotWithClosePopups - } - opts.ScreenShotOptions.ScreenShotWithOCRCluster = r.ScreenShotWithOCRCluster - opts.ScreenShotOptions.ScreenShotFileName = r.ScreenShotFileName - - // Copy screen record options (ScreenRecordOptions) - if r.ScreenRecordDuration != nil { - opts.ScreenRecordOptions.ScreenRecordDuration = *r.ScreenRecordDuration - } - if r.ScreenRecordWithAudio != nil { - opts.ScreenRecordOptions.ScreenRecordWithAudio = *r.ScreenRecordWithAudio - } - if r.ScreenRecordWithScrcpy != nil { - opts.ScreenRecordOptions.ScreenRecordWithScrcpy = *r.ScreenRecordWithScrcpy - } - opts.ScreenRecordOptions.ScreenRecordPath = r.ScreenRecordPath - - // Copy mark operation options (MarkOperationOptions) - if r.PreMarkOperation != nil { - opts.MarkOperationOptions.PreMarkOperation = *r.PreMarkOperation - } - if r.PostMarkOperation != nil { - opts.MarkOperationOptions.PostMarkOperation = *r.PostMarkOperation - } - - return opts -} - -// GetMCPOptions generates MCP tool options for specific action types -func (r *UnifiedActionRequest) GetMCPOptions(actionType ActionMethod) []mcp.ToolOption { - // Define field mappings for different action types - fieldMappings := map[ActionMethod][]string{ - ACTION_TapXY: {"platform", "serial", "x", "y", "duration"}, - ACTION_TapAbsXY: {"platform", "serial", "x", "y", "duration"}, - ACTION_TapByOCR: {"platform", "serial", "text", "ignoreNotFoundError", "maxRetryTimes", "index", "regex", "tapRandomRect"}, - ACTION_TapByCV: {"platform", "serial", "ignoreNotFoundError", "maxRetryTimes", "index", "tapRandomRect"}, - ACTION_DoubleTapXY: {"platform", "serial", "x", "y"}, - ACTION_SwipeDirection: {"platform", "serial", "direction", "duration", "pressDuration"}, - ACTION_SwipeCoordinate: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"}, - ACTION_Swipe: {"platform", "serial", "direction", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"}, - ACTION_Drag: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"}, - ACTION_Input: {"platform", "serial", "text", "frequency"}, - ACTION_AppLaunch: {"platform", "serial", "packageName"}, - ACTION_AppTerminate: {"platform", "serial", "packageName"}, - ACTION_AppInstall: {"platform", "serial", "appUrl", "packageName"}, - ACTION_AppUninstall: {"platform", "serial", "packageName"}, - ACTION_AppClear: {"platform", "serial", "packageName"}, - ACTION_PressButton: {"platform", "serial", "button"}, - ACTION_SwipeToTapApp: {"platform", "serial", "appName", "ignoreNotFoundError", "maxRetryTimes", "index"}, - ACTION_SwipeToTapText: {"platform", "serial", "text", "ignoreNotFoundError", "maxRetryTimes", "index", "regex"}, - ACTION_SwipeToTapTexts: {"platform", "serial", "texts", "ignoreNotFoundError", "maxRetryTimes", "index", "regex"}, - ACTION_SecondaryClick: {"platform", "serial", "x", "y"}, - ACTION_HoverBySelector: {"platform", "serial", "selector"}, - ACTION_TapBySelector: {"platform", "serial", "selector"}, - ACTION_SecondaryClickBySelector: {"platform", "serial", "selector"}, - ACTION_WebCloseTab: {"platform", "serial", "tabIndex"}, - ACTION_WebLoginNoneUI: {"platform", "serial", "packageName", "phoneNumber", "captcha", "password"}, - ACTION_SetIme: {"platform", "serial", "ime"}, - ACTION_GetSource: {"platform", "serial", "packageName"}, - ACTION_Sleep: {"seconds"}, - ACTION_SleepMS: {"platform", "serial", "milliseconds"}, - ACTION_SleepRandom: {"platform", "serial", "params"}, - ACTION_AIAction: {"platform", "serial", "prompt"}, - ACTION_Finished: {"content"}, - ACTION_ListAvailableDevices: {}, - ACTION_SelectDevice: {"platform", "serial"}, - ACTION_ScreenShot: {"platform", "serial"}, - ACTION_GetScreenSize: {"platform", "serial"}, - ACTION_Home: {"platform", "serial"}, - ACTION_Back: {"platform", "serial"}, - ACTION_ListPackages: {"platform", "serial"}, - ACTION_ClosePopups: {"platform", "serial"}, - } - - fields := fieldMappings[actionType] - if fields == nil { - // Fallback to all fields if not specifically mapped - return NewMCPOptions(*r) - } - - // Generate options only for specified fields - return r.generateMCPOptionsForFields(fields) -} - -// generateMCPOptionsForFields generates MCP options for specific fields -func (r *UnifiedActionRequest) generateMCPOptionsForFields(fields []string) []mcp.ToolOption { - options := make([]mcp.ToolOption, 0) - rType := reflect.TypeOf(*r) - rValue := reflect.ValueOf(*r) - - fieldMap := make(map[string]reflect.StructField) - for i := 0; i < rType.NumField(); i++ { - field := rType.Field(i) - jsonTag := field.Tag.Get("json") - if jsonTag != "" && jsonTag != "-" { - name := strings.Split(jsonTag, ",")[0] - fieldMap[name] = field - } - } - - for _, fieldName := range fields { - field, exists := fieldMap[fieldName] - if !exists { - continue - } - - jsonTag := field.Tag.Get("json") - if jsonTag == "" || jsonTag == "-" { - continue - } - name := strings.Split(jsonTag, ",")[0] - binding := field.Tag.Get("binding") - required := strings.Contains(binding, "required") - desc := field.Tag.Get("desc") - - // Check if field has a value - fieldValue := rValue.FieldByName(field.Name) - if !fieldValue.IsValid() { - continue - } - - // Handle pointer types - fieldType := field.Type - isPointer := false - if fieldType.Kind() == reflect.Ptr { - isPointer = true - fieldType = fieldType.Elem() - } - - // Skip nil pointer fields if not required - if isPointer && fieldValue.IsNil() && !required { - continue - } - - switch fieldType.Kind() { - case reflect.Float64, reflect.Float32, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if required { - options = append(options, mcp.WithNumber(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithNumber(name, mcp.Description(desc))) - } - case reflect.String: - if required { - options = append(options, mcp.WithString(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithString(name, mcp.Description(desc))) - } - case reflect.Bool: - if required { - options = append(options, mcp.WithBoolean(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithBoolean(name, mcp.Description(desc))) - } - case reflect.Slice: - if fieldType.Elem().Kind() == reflect.String || fieldType.Elem().Kind() == reflect.Float64 { - if required { - options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc))) - } else { - options = append(options, mcp.WithArray(name, mcp.Description(desc))) - } - } - case reflect.Map, reflect.Interface: - // Skip map and interface types for now - continue - default: - log.Warn().Str("field_type", fieldType.String()).Msg("Unsupported field type") - } - } - - return options -} diff --git a/uixt/option/request_test.go b/uixt/option/request_test.go deleted file mode 100644 index 10dad159..00000000 --- a/uixt/option/request_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package option - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestUnifiedActionRequest_ToActionOptions(t *testing.T) { - // Test TapXY request conversion - x := 0.5 - y := 0.7 - duration := 1.0 - maxRetryTimes := 3 - regex := true - - unifiedReq := &UnifiedActionRequest{ - Platform: "android", - Serial: "device123", - X: &x, - Y: &y, - Duration: &duration, - MaxRetryTimes: &maxRetryTimes, - Regex: ®ex, - } - - actionOpts := unifiedReq.ToActionOptions() - - assert.Equal(t, 1.0, actionOpts.Duration) - assert.Equal(t, 3, actionOpts.MaxRetryTimes) - assert.True(t, actionOpts.Regex) -} - -func TestUnifiedActionRequest_GetMCPOptions(t *testing.T) { - unifiedReq := &UnifiedActionRequest{ - Platform: "android", - Serial: "device123", - } - - // Test TapXY options - tapOptions := unifiedReq.GetMCPOptions(ACTION_TapXY) - assert.NotEmpty(t, tapOptions) - - // Test TapByOCR options - ocrOptions := unifiedReq.GetMCPOptions(ACTION_TapByOCR) - assert.NotEmpty(t, ocrOptions) - - // Test unknown action (should fallback to all fields) - unknownOptions := unifiedReq.GetMCPOptions("unknown_action") - assert.NotEmpty(t, unknownOptions) -} - -func TestUnifiedActionRequest_SwipeDirection(t *testing.T) { - duration := 2.0 - pressDuration := 0.5 - - unifiedReq := &UnifiedActionRequest{ - Platform: "android", - Serial: "device123", - Direction: "up", - Duration: &duration, - PressDuration: &pressDuration, - } - - actionOpts := unifiedReq.ToActionOptions() - assert.Equal(t, "up", actionOpts.Direction) - assert.Equal(t, 2.0, actionOpts.Duration) - assert.Equal(t, 0.5, actionOpts.PressDuration) -} - -func TestUnifiedActionRequest_SwipeCoordinate(t *testing.T) { - params := []float64{0.2, 0.8, 0.2, 0.2} - - unifiedReq := &UnifiedActionRequest{ - Platform: "android", - Serial: "device123", - Params: params, - } - - actionOpts := unifiedReq.ToActionOptions() - assert.Equal(t, params, actionOpts.Direction) -} - -func TestUnifiedActionRequest_ScreenOptions(t *testing.T) { - ocrEnabled := true - uploadEnabled := true - uiTypes := []string{"button", "text"} - - unifiedReq := &UnifiedActionRequest{ - Platform: "android", - Serial: "device123", - ScreenShotWithOCR: &ocrEnabled, - ScreenShotWithUpload: &uploadEnabled, - ScreenShotWithUITypes: uiTypes, - } - - actionOpts := unifiedReq.ToActionOptions() - assert.True(t, actionOpts.ScreenShotWithOCR) - assert.True(t, actionOpts.ScreenShotWithUpload) - assert.Equal(t, uiTypes, actionOpts.ScreenShotWithUITypes) -} - -func TestUnifiedActionRequest_NilPointerSafety(t *testing.T) { - // Test with nil pointers - unifiedReq := &UnifiedActionRequest{ - Platform: "android", - Serial: "device123", - // All pointer fields are nil - } - - actionOpts := unifiedReq.ToActionOptions() - assert.Equal(t, 0, actionOpts.MaxRetryTimes) - assert.Equal(t, 0.0, actionOpts.Duration) - assert.Equal(t, 0.0, actionOpts.PressDuration) - assert.False(t, actionOpts.Regex) - assert.False(t, actionOpts.TapRandomRect) -} - -func TestUnifiedActionRequest_CustomOptions(t *testing.T) { - customData := map[string]interface{}{ - "custom_key": "custom_value", - "number": 42, - } - - unifiedReq := &UnifiedActionRequest{ - Platform: "android", - Serial: "device123", - Custom: customData, - } - - actionOpts := unifiedReq.ToActionOptions() - assert.Equal(t, customData, actionOpts.Custom) -} diff --git a/uixt/sdk.go b/uixt/sdk.go index f3440f87..d55c26d5 100644 --- a/uixt/sdk.go +++ b/uixt/sdk.go @@ -62,7 +62,7 @@ func (c *MCPClient4XTDriver) ListTools(ctx context.Context, req mcp.ListToolsReq } func (c *MCPClient4XTDriver) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - actionTool := c.Server.GetToolByAction(option.ActionMethod(req.Params.Name)) + actionTool := c.Server.GetToolByAction(option.ActionName(req.Params.Name)) if actionTool == nil { return mcp.NewToolResultError(fmt.Sprintf("action %s for tool not found", req.Params.Name)), nil }