From 7fb966b7ba2812bec9b53eb561f4ce444cd65658 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Tue, 27 May 2025 11:49:30 +0800 Subject: [PATCH] refactor: improve ActionMethod type safety and eliminate type conversions --- docs/cmd/hrp.md | 2 +- docs/cmd/hrp_adb.md | 2 +- docs/cmd/hrp_adb_devices.md | 2 +- docs/cmd/hrp_adb_install.md | 2 +- docs/cmd/hrp_adb_screencap.md | 2 +- docs/cmd/hrp_build.md | 2 +- docs/cmd/hrp_convert.md | 2 +- docs/cmd/hrp_ios.md | 2 +- docs/cmd/hrp_ios_apps.md | 2 +- docs/cmd/hrp_ios_devices.md | 2 +- docs/cmd/hrp_ios_install.md | 2 +- docs/cmd/hrp_ios_mount.md | 2 +- docs/cmd/hrp_ios_ps.md | 2 +- docs/cmd/hrp_ios_reboot.md | 2 +- docs/cmd/hrp_ios_tunnel.md | 2 +- docs/cmd/hrp_ios_uninstall.md | 2 +- docs/cmd/hrp_ios_xctest.md | 2 +- docs/cmd/hrp_mcp-server.md | 2 +- docs/cmd/hrp_mcphost.md | 2 +- docs/cmd/hrp_pytest.md | 2 +- docs/cmd/hrp_run.md | 2 +- docs/cmd/hrp_server.md | 2 +- docs/cmd/hrp_startproject.md | 2 +- docs/cmd/hrp_wiki.md | 2 +- internal/version/VERSION | 2 +- server/app.go | 49 +- server/key.go | 21 +- server/model.go | 9 - server/ui.go | 104 +- server/ui_test.go | 20 +- uixt/mcp_server.go | 473 +++++----- uixt/option/action.go | 11 + uixt/option/migration_summary.md | 1 - uixt/option/request.go | 889 +++++++++++++----- ...nified_request_test.go => request_test.go} | 73 -- uixt/option/unified_request.go | 350 ------- 36 files changed, 1087 insertions(+), 963 deletions(-) delete mode 100644 uixt/option/migration_summary.md rename uixt/option/{unified_request_test.go => request_test.go} (59%) delete mode 100644 uixt/option/unified_request.go diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 031ab892..c0373f60 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -62,4 +62,4 @@ Copyright © 2017-present debugtalk. Apache-2.0 License. * [hrp startproject](hrp_startproject.md) - Create a scaffold project * [hrp wiki](hrp_wiki.md) - visit https://httprunner.com -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_adb.md b/docs/cmd/hrp_adb.md index 2cfe716d..bc05c1b6 100644 --- a/docs/cmd/hrp_adb.md +++ b/docs/cmd/hrp_adb.md @@ -23,4 +23,4 @@ simple utils for android device management * [hrp adb install](hrp_adb_install.md) - push package to the device and install them automatically * [hrp adb screencap](hrp_adb_screencap.md) - Start android screen capture -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_adb_devices.md b/docs/cmd/hrp_adb_devices.md index b044f51c..256a5881 100644 --- a/docs/cmd/hrp_adb_devices.md +++ b/docs/cmd/hrp_adb_devices.md @@ -24,4 +24,4 @@ hrp adb devices [flags] * [hrp adb](hrp_adb.md) - simple utils for android device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_adb_install.md b/docs/cmd/hrp_adb_install.md index c6100974..3cbdeb7a 100644 --- a/docs/cmd/hrp_adb_install.md +++ b/docs/cmd/hrp_adb_install.md @@ -28,4 +28,4 @@ hrp adb install [flags] PACKAGE * [hrp adb](hrp_adb.md) - simple utils for android device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_adb_screencap.md b/docs/cmd/hrp_adb_screencap.md index 1e0d8fb0..5ff162e5 100644 --- a/docs/cmd/hrp_adb_screencap.md +++ b/docs/cmd/hrp_adb_screencap.md @@ -25,4 +25,4 @@ hrp adb screencap [flags] * [hrp adb](hrp_adb.md) - simple utils for android device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_build.md b/docs/cmd/hrp_build.md index 2c0d5091..39c1b593 100644 --- a/docs/cmd/hrp_build.md +++ b/docs/cmd/hrp_build.md @@ -36,4 +36,4 @@ hrp build $path ... [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md index da4fa0dc..ac5e4123 100644 --- a/docs/cmd/hrp_convert.md +++ b/docs/cmd/hrp_convert.md @@ -34,4 +34,4 @@ hrp convert $path... [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios.md b/docs/cmd/hrp_ios.md index d181cb73..28ca7fb1 100644 --- a/docs/cmd/hrp_ios.md +++ b/docs/cmd/hrp_ios.md @@ -29,4 +29,4 @@ simple utils for ios device management * [hrp ios uninstall](hrp_ios_uninstall.md) - uninstall package automatically * [hrp ios xctest](hrp_ios_xctest.md) - run xctest -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_apps.md b/docs/cmd/hrp_ios_apps.md index 647ce4a8..6bdda59d 100644 --- a/docs/cmd/hrp_ios_apps.md +++ b/docs/cmd/hrp_ios_apps.md @@ -26,4 +26,4 @@ hrp ios apps [flags] * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_devices.md b/docs/cmd/hrp_ios_devices.md index b069924a..2446ee24 100644 --- a/docs/cmd/hrp_ios_devices.md +++ b/docs/cmd/hrp_ios_devices.md @@ -24,4 +24,4 @@ hrp ios devices [flags] * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_install.md b/docs/cmd/hrp_ios_install.md index 560dfd4a..fcca7183 100644 --- a/docs/cmd/hrp_ios_install.md +++ b/docs/cmd/hrp_ios_install.md @@ -25,4 +25,4 @@ hrp ios install [flags] PACKAGE * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_mount.md b/docs/cmd/hrp_ios_mount.md index 2baf36ee..f5428f36 100644 --- a/docs/cmd/hrp_ios_mount.md +++ b/docs/cmd/hrp_ios_mount.md @@ -28,4 +28,4 @@ hrp ios mount [flags] * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_ps.md b/docs/cmd/hrp_ios_ps.md index 2326472b..33354653 100644 --- a/docs/cmd/hrp_ios_ps.md +++ b/docs/cmd/hrp_ios_ps.md @@ -26,4 +26,4 @@ hrp ios ps [flags] * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_reboot.md b/docs/cmd/hrp_ios_reboot.md index 19d4b9fd..ef244eed 100644 --- a/docs/cmd/hrp_ios_reboot.md +++ b/docs/cmd/hrp_ios_reboot.md @@ -25,4 +25,4 @@ hrp ios reboot [flags] * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_tunnel.md b/docs/cmd/hrp_ios_tunnel.md index 93208f14..f800233b 100644 --- a/docs/cmd/hrp_ios_tunnel.md +++ b/docs/cmd/hrp_ios_tunnel.md @@ -24,4 +24,4 @@ hrp ios tunnel [flags] * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_uninstall.md b/docs/cmd/hrp_ios_uninstall.md index a67e24ed..1253681f 100644 --- a/docs/cmd/hrp_ios_uninstall.md +++ b/docs/cmd/hrp_ios_uninstall.md @@ -26,4 +26,4 @@ hrp ios uninstall [flags] PACKAGE * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_ios_xctest.md b/docs/cmd/hrp_ios_xctest.md index 3cad34b6..295ee88b 100644 --- a/docs/cmd/hrp_ios_xctest.md +++ b/docs/cmd/hrp_ios_xctest.md @@ -28,4 +28,4 @@ hrp ios xctest [flags] * [hrp ios](hrp_ios.md) - simple utils for ios device management -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_mcp-server.md b/docs/cmd/hrp_mcp-server.md index f663e1b1..24052b1d 100644 --- a/docs/cmd/hrp_mcp-server.md +++ b/docs/cmd/hrp_mcp-server.md @@ -28,4 +28,4 @@ hrp mcp-server [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_mcphost.md b/docs/cmd/hrp_mcphost.md index 15b4d79b..fd0d19da 100644 --- a/docs/cmd/hrp_mcphost.md +++ b/docs/cmd/hrp_mcphost.md @@ -31,4 +31,4 @@ hrp mcphost [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_pytest.md b/docs/cmd/hrp_pytest.md index 6c164f03..a1a9cbcc 100644 --- a/docs/cmd/hrp_pytest.md +++ b/docs/cmd/hrp_pytest.md @@ -24,4 +24,4 @@ hrp pytest $path ... [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index fffde269..062ebdeb 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -44,4 +44,4 @@ hrp run $path... [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_server.md b/docs/cmd/hrp_server.md index 931b4336..ce091477 100644 --- a/docs/cmd/hrp_server.md +++ b/docs/cmd/hrp_server.md @@ -30,4 +30,4 @@ hrp server start [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index 0a3d8386..d4480d50 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -29,4 +29,4 @@ hrp startproject $project_name [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/docs/cmd/hrp_wiki.md b/docs/cmd/hrp_wiki.md index 5c8226b3..a093892a 100644 --- a/docs/cmd/hrp_wiki.md +++ b/docs/cmd/hrp_wiki.md @@ -24,4 +24,4 @@ hrp wiki [flags] * [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance -###### Auto generated by spf13/cobra on 25-May-2025 +###### Auto generated by spf13/cobra on 26-May-2025 diff --git a/internal/version/VERSION b/internal/version/VERSION index 7ce94e90..e3542c4b 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2505262313 +v5.0.0-beta-2505271149 diff --git a/server/app.go b/server/app.go index 10357192..951e26db 100644 --- a/server/app.go +++ b/server/app.go @@ -22,17 +22,27 @@ func (r *Router) foregroundAppHandler(c *gin.Context) { } func (r *Router) appInfoHandler(c *gin.Context) { - var appInfoReq option.AppInfoRequest - if err := c.ShouldBindQuery(&appInfoReq); err != nil { + var req option.UnifiedActionRequest + if err := c.ShouldBindQuery(&req); err != nil { RenderErrorValidateRequest(c, err) return } + + // Set platform and serial from URL parameters + setRequestContextFromURL(c, &req) + + // Validate for HTTP API usage + if err := req.ValidateForHTTPAPI(option.ACTION_AppInfo); err != nil { + RenderErrorValidateRequest(c, err) + return + } + device, err := r.GetDevice(c) if err != nil { return } if androidDevice, ok := device.(*uixt.AndroidDevice); ok { - appInfo, err := androidDevice.GetAppInfo(appInfoReq.PackageName) + appInfo, err := androidDevice.GetAppInfo(req.PackageName) if err != nil { RenderError(c, err) return @@ -40,7 +50,7 @@ func (r *Router) appInfoHandler(c *gin.Context) { RenderSuccess(c, appInfo) return } else if iOSDevice, ok := device.(*uixt.IOSDevice); ok { - appInfo, err := iOSDevice.GetAppInfo(appInfoReq.PackageName) + appInfo, err := iOSDevice.GetAppInfo(req.PackageName) if err != nil { RenderError(c, err) return @@ -51,9 +61,8 @@ func (r *Router) appInfoHandler(c *gin.Context) { } func (r *Router) clearAppHandler(c *gin.Context) { - var appClearReq option.AppClearRequest - if err := c.ShouldBindJSON(&appClearReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_AppClear) + if err != nil { return } @@ -61,7 +70,7 @@ func (r *Router) clearAppHandler(c *gin.Context) { if err != nil { return } - err = driver.AppClear(appClearReq.PackageName) + err = driver.AppClear(req.PackageName) if err != nil { RenderError(c, err) return @@ -70,16 +79,16 @@ func (r *Router) clearAppHandler(c *gin.Context) { } func (r *Router) launchAppHandler(c *gin.Context) { - var appLaunchReq option.AppLaunchRequest - if err := c.ShouldBindJSON(&appLaunchReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_AppLaunch) + if err != nil { return } + driver, err := r.GetDriver(c) if err != nil { return } - err = driver.AppLaunch(appLaunchReq.PackageName) + err = driver.AppLaunch(req.PackageName) if err != nil { RenderError(c, err) return @@ -88,16 +97,16 @@ func (r *Router) launchAppHandler(c *gin.Context) { } func (r *Router) terminalAppHandler(c *gin.Context) { - var appTerminateReq option.AppTerminateRequest - if err := c.ShouldBindJSON(&appTerminateReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_AppTerminate) + if err != nil { return } + driver, err := r.GetDriver(c) if err != nil { return } - _, err = driver.AppTerminate(appTerminateReq.PackageName) + _, err = driver.AppTerminate(req.PackageName) if err != nil { RenderError(c, err) return @@ -106,16 +115,16 @@ func (r *Router) terminalAppHandler(c *gin.Context) { } func (r *Router) uninstallAppHandler(c *gin.Context) { - var appUninstallReq option.AppUninstallRequest - if err := c.ShouldBindJSON(&appUninstallReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_AppUninstall) + if err != nil { return } + driver, err := r.GetDriver(c) if err != nil { return } - err = driver.GetDevice().Uninstall(appUninstallReq.PackageName) + err = driver.GetDevice().Uninstall(req.PackageName) if err != nil { log.Err(err).Msg("failed to uninstall app") } diff --git a/server/key.go b/server/key.go index 272dd5eb..bf585cdc 100644 --- a/server/key.go +++ b/server/key.go @@ -34,19 +34,20 @@ func (r *Router) homeHandler(c *gin.Context) { } func (r *Router) backspaceHandler(c *gin.Context) { - var deleteReq option.DeleteRequest - if err := c.ShouldBindJSON(&deleteReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_Backspace) + if err != nil { return } - if deleteReq.Count == 0 { - deleteReq.Count = 20 + + count := req.GetCount() + if count == 0 { + count = 20 } driver, err := r.GetDriver(c) if err != nil { return } - err = driver.Backspace(deleteReq.Count) + err = driver.Backspace(count) if err != nil { RenderError(c, err) return @@ -55,18 +56,18 @@ func (r *Router) backspaceHandler(c *gin.Context) { } func (r *Router) keycodeHandler(c *gin.Context) { - var keycodeReq option.KeycodeRequest - if err := c.ShouldBindJSON(&keycodeReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_KeyCode) + if err != nil { return } + driver, err := r.GetDriver(c) if err != nil { return } // TODO FIXME err = driver.IDriver.(*uixt.ADBDriver). - PressKeyCode(uixt.KeyCode(keycodeReq.Keycode), uixt.KMEmpty) + PressKeyCode(uixt.KeyCode(req.GetKeycode()), uixt.KMEmpty) if err != nil { RenderError(c, err) return diff --git a/server/model.go b/server/model.go index 315db816..969ca9d2 100644 --- a/server/model.go +++ b/server/model.go @@ -33,15 +33,6 @@ type UploadRequest struct { FileFormat string `json:"file_format"` } -type HoverRequest struct { - X float64 `json:"x"` - Y float64 `json:"y"` -} - -type ScrollRequest struct { - Delta int `json:"delta"` -} - type CreateBrowserRequest struct { Timeout int `json:"timeout"` Width int `json:"width"` diff --git a/server/ui.go b/server/ui.go index 83a45563..b3f1d363 100644 --- a/server/ui.go +++ b/server/ui.go @@ -6,21 +6,55 @@ import ( "github.com/httprunner/httprunner/v5/uixt/option" ) -func (r *Router) tapHandler(c *gin.Context) { - var tapReq option.TapRequest - if err := c.ShouldBindJSON(&tapReq); err != nil { +// 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 + + // Bind JSON request + if err := c.ShouldBindJSON(&req); err != nil { RenderErrorValidateRequest(c, err) - return + return nil, err } + + // Set platform and serial from URL parameters + setRequestContextFromURL(c, &req) + + // Validate for HTTP API usage + if err := req.ValidateForHTTPAPI(actionType); err != nil { + RenderErrorValidateRequest(c, err) + return nil, err + } + + return &req, nil +} + +// setRequestContextFromURL sets platform and serial from URL parameters +func setRequestContextFromURL(c *gin.Context, req *option.UnifiedActionRequest) { + if req.Platform == "" { + req.Platform = c.Param("platform") + } + if req.Serial == "" { + req.Serial = c.Param("serial") + } +} + +func (r *Router) tapHandler(c *gin.Context) { + req, err := r.processUnifiedRequest(c, option.ACTION_Tap) + if err != nil { + return // Error already handled in processUnifiedRequest + } + driver, err := r.GetDriver(c) if err != nil { return } - if tapReq.Duration > 0 { - err = driver.Drag(tapReq.X, tapReq.Y, tapReq.X, tapReq.Y, - option.WithDuration(tapReq.Duration)) + + // Use UnifiedActionRequest directly + if req.GetDuration() > 0 { + err = driver.Drag(req.GetX(), req.GetY(), req.GetX(), req.GetY(), + option.WithDuration(req.GetDuration())) } else { - err = driver.TapXY(tapReq.X, tapReq.Y) + err = driver.TapXY(req.GetX(), req.GetY()) } if err != nil { RenderError(c, err) @@ -30,17 +64,17 @@ func (r *Router) tapHandler(c *gin.Context) { } func (r *Router) rightClickHandler(c *gin.Context) { - var rightClickReq option.TapRequest - if err := c.ShouldBindJSON(&rightClickReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_RightClick) + if err != nil { return } + driver, err := r.GetDriver(c) if err != nil { return } err = driver.IDriver.(*uixt.BrowserDriver). - SecondaryClick(rightClickReq.X, rightClickReq.Y) + SecondaryClick(req.GetX(), req.GetY()) if err != nil { RenderError(c, err) return @@ -71,9 +105,8 @@ func (r *Router) uploadHandler(c *gin.Context) { } func (r *Router) hoverHandler(c *gin.Context) { - var hoverReq HoverRequest - if err := c.ShouldBindJSON(&hoverReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_Hover) + if err != nil { return } @@ -84,7 +117,7 @@ func (r *Router) hoverHandler(c *gin.Context) { } err = driver.IDriver.(*uixt.BrowserDriver). - Hover(hoverReq.X, hoverReq.Y) + Hover(req.GetX(), req.GetY()) if err != nil { RenderError(c, err) @@ -94,9 +127,8 @@ func (r *Router) hoverHandler(c *gin.Context) { } func (r *Router) scrollHandler(c *gin.Context) { - var scrollReq ScrollRequest - if err := c.ShouldBindJSON(&scrollReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_Scroll) + if err != nil { return } @@ -107,7 +139,7 @@ func (r *Router) scrollHandler(c *gin.Context) { } err = driver.IDriver.(*uixt.BrowserDriver). - Scroll(scrollReq.Delta) + Scroll(req.GetDelta()) if err != nil { RenderError(c, err) @@ -117,9 +149,8 @@ func (r *Router) scrollHandler(c *gin.Context) { } func (r *Router) doubleTapHandler(c *gin.Context) { - var tapReq option.TapRequest - if err := c.ShouldBindJSON(&tapReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_DoubleTap) + if err != nil { return } @@ -128,7 +159,7 @@ func (r *Router) doubleTapHandler(c *gin.Context) { return } - err = driver.DoubleTap(tapReq.X, tapReq.Y) + err = driver.DoubleTap(req.GetX(), req.GetY()) if err != nil { RenderError(c, err) return @@ -137,22 +168,23 @@ func (r *Router) doubleTapHandler(c *gin.Context) { } func (r *Router) dragHandler(c *gin.Context) { - var dragReq option.DragRequest - if err := c.ShouldBindJSON(&dragReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_Drag) + if err != nil { return } - if dragReq.Duration == 0 { - dragReq.Duration = 1 + + duration := req.GetDuration() + if duration == 0 { + duration = 1 } driver, err := r.GetDriver(c) if err != nil { return } - err = driver.Drag(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY, - option.WithDuration(dragReq.Duration), - option.WithPressDuration(dragReq.PressDuration)) + err = driver.Drag(req.GetFromX(), req.GetFromY(), req.GetToX(), req.GetToY(), + option.WithDuration(duration), + option.WithPressDuration(req.GetPressDuration())) if err != nil { RenderError(c, err) return @@ -161,16 +193,16 @@ func (r *Router) dragHandler(c *gin.Context) { } func (r *Router) inputHandler(c *gin.Context) { - var inputReq option.InputRequest - if err := c.ShouldBindJSON(&inputReq); err != nil { - RenderErrorValidateRequest(c, err) + req, err := r.processUnifiedRequest(c, option.ACTION_Input) + if err != nil { return } + driver, err := r.GetDriver(c) if err != nil { return } - err = driver.Input(inputReq.Text, option.WithFrequency(inputReq.Frequency)) + err = driver.Input(req.Text, option.WithFrequency(req.GetFrequency())) if err != nil { RenderError(c, err) return diff --git a/server/ui_test.go b/server/ui_test.go index 793112f2..146d984d 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 - tapReq option.TapRequest + req option.UnifiedActionRequest wantStatus int wantResp HttpResponse }{ { name: "tap abs xy", path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), - tapReq: option.TapRequest{ - X: 500, - Y: 800, - Duration: 0, + req: option.UnifiedActionRequest{ + X: &[]float64{500}[0], + Y: &[]float64{800}[0], + Duration: &[]float64{0}[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"), - tapReq: option.TapRequest{ - X: 0.5, - Y: 0.6, - Duration: 0, + req: option.UnifiedActionRequest{ + X: &[]float64{0.5}[0], + Y: &[]float64{0.6}[0], + Duration: &[]float64{0}[0], }, wantStatus: http.StatusOK, wantResp: HttpResponse{ @@ -56,7 +56,7 @@ func TestTapHandler(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - reqBody, _ := json.Marshal(tt.tapReq) + reqBody, _ := json.Marshal(tt.req) req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewBuffer(reqBody)) req.Header.Set("Content-Type", "application/json") diff --git a/uixt/mcp_server.go b/uixt/mcp_server.go index fd048043..953b605b 100644 --- a/uixt/mcp_server.go +++ b/uixt/mcp_server.go @@ -382,28 +382,32 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var tapAbsReq option.TapAbsXYRequest - if err := mapToStruct(request.Params.Arguments, &tapAbsReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - // Build action options from request structure - var opts []option.ActionOption + // Convert to ActionOptions + actionOpts := unifiedReq.ToActionOptions() + opts := actionOpts.Options() - // Add numeric options - if tapAbsReq.Duration > 0 { - opts = append(opts, option.WithDuration(tapAbsReq.Duration)) + // Add default options + opts = append(opts, option.WithPreMarkOperation(true)) + + // Validate required parameters + if unifiedReq.X == nil || unifiedReq.Y == nil { + return nil, fmt.Errorf("x and y coordinates are required") } // Tap absolute XY action logic - log.Info().Float64("x", tapAbsReq.X).Float64("y", tapAbsReq.Y).Msg("tapping at absolute coordinates") + log.Info().Float64("x", *unifiedReq.X).Float64("y", *unifiedReq.Y).Msg("tapping at absolute coordinates") - err = driverExt.TapAbsXY(tapAbsReq.X, tapAbsReq.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)", tapAbsReq.X, tapAbsReq.Y)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped at absolute coordinates (%.0f, %.0f)", *unifiedReq.X, *unifiedReq.Y)), nil } } @@ -450,41 +454,31 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var ocrReq option.TapByOCRRequest - if err := mapToStruct(request.Params.Arguments, &ocrReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - // Build action options from request structure - var opts []option.ActionOption + // Convert to ActionOptions + actionOpts := unifiedReq.ToActionOptions() + opts := actionOpts.Options() - // Add boolean options - if ocrReq.IgnoreNotFoundError { - opts = append(opts, option.WithIgnoreNotFoundError(true)) - } - if ocrReq.Regex { - opts = append(opts, option.WithRegex(true)) - } - if ocrReq.TapRandomRect { - opts = append(opts, option.WithTapRandomRect(true)) - } + // Add default options + opts = append(opts, option.WithPreMarkOperation(true)) - // Add numeric options - if ocrReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(ocrReq.MaxRetryTimes)) - } - if ocrReq.Index > 0 { - opts = append(opts, option.WithIndex(ocrReq.Index)) + // Validate required parameters + if unifiedReq.Text == "" { + return nil, fmt.Errorf("text parameter is required") } // Tap by OCR action logic - log.Info().Str("text", ocrReq.Text).Msg("tapping by OCR") - err = driverExt.TapByOCR(ocrReq.Text, opts...) + log.Info().Str("text", unifiedReq.Text).Msg("tapping by OCR") + err = driverExt.TapByOCR(unifiedReq.Text, opts...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Tap by OCR failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped on OCR text: %s", ocrReq.Text)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped on OCR text: %s", unifiedReq.Text)), nil } } @@ -525,32 +519,20 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var cvReq option.TapByCVRequest - if err := mapToStruct(request.Params.Arguments, &cvReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - // Build action options from request structure - var opts []option.ActionOption + // Convert to ActionOptions + actionOpts := unifiedReq.ToActionOptions() + opts := actionOpts.Options() - // Add boolean options - if cvReq.IgnoreNotFoundError { - opts = append(opts, option.WithIgnoreNotFoundError(true)) - } - if cvReq.TapRandomRect { - opts = append(opts, option.WithTapRandomRect(true)) - } - - // Add numeric options - if cvReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(cvReq.MaxRetryTimes)) - } - if cvReq.Index > 0 { - opts = append(opts, option.WithIndex(cvReq.Index)) - } + // Add default options + opts = append(opts, option.WithPreMarkOperation(true)) // Tap by CV action logic - log.Info().Str("imagePath", cvReq.ImagePath).Msg("tapping by CV") + log.Info().Msg("tapping by CV") // For TapByCV, we need to check if there are UI types in the options // In the original DoAction, it requires ScreenShotWithUITypes to be set @@ -599,19 +581,24 @@ func (t *ToolDoubleTapXY) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var doubleTapReq option.DoubleTapXYRequest - if err := mapToStruct(request.Params.Arguments, &doubleTapReq); err != nil { + var unifiedReq option.UnifiedActionRequest + 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 { + return nil, fmt.Errorf("x and y coordinates are required") + } + // Double tap XY action logic - log.Info().Float64("x", doubleTapReq.X).Float64("y", doubleTapReq.Y).Msg("double tapping at coordinates") - err = driverExt.DoubleTap(doubleTapReq.X, doubleTapReq.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)", doubleTapReq.X, doubleTapReq.Y)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully double tapped at (%.2f, %.2f)", *unifiedReq.X, *unifiedReq.Y)), nil } } @@ -685,23 +672,23 @@ func (t *ToolLaunchApp) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var appLaunchReq option.AppLaunchRequest - if err := mapToStruct(request.Params.Arguments, &appLaunchReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - if appLaunchReq.PackageName == "" { + if unifiedReq.PackageName == "" { return nil, fmt.Errorf("package_name is required") } // Launch app action logic - log.Info().Str("packageName", appLaunchReq.PackageName).Msg("launching app") - err = driverExt.AppLaunch(appLaunchReq.PackageName) + log.Info().Str("packageName", unifiedReq.PackageName).Msg("launching app") + err = driverExt.AppLaunch(unifiedReq.PackageName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Launch app failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully launched app: %s", appLaunchReq.PackageName)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully launched app: %s", unifiedReq.PackageName)), nil } } @@ -738,26 +725,26 @@ func (t *ToolTerminateApp) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var appTerminateReq option.AppTerminateRequest - if err := mapToStruct(request.Params.Arguments, &appTerminateReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - if appTerminateReq.PackageName == "" { + if unifiedReq.PackageName == "" { return nil, fmt.Errorf("package_name is required") } // Terminate app action logic - log.Info().Str("packageName", appTerminateReq.PackageName).Msg("terminating app") - success, err := driverExt.AppTerminate(appTerminateReq.PackageName) + log.Info().Str("packageName", unifiedReq.PackageName).Msg("terminating app") + success, err := driverExt.AppTerminate(unifiedReq.PackageName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Terminate app failed: %s", err.Error())), nil } if !success { - log.Warn().Str("packageName", appTerminateReq.PackageName).Msg("app was not running") + log.Warn().Str("packageName", unifiedReq.PackageName).Msg("app was not running") } - return mcp.NewToolResultText(fmt.Sprintf("Successfully terminated app: %s", appTerminateReq.PackageName)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully terminated app: %s", unifiedReq.PackageName)), nil } } @@ -868,19 +855,19 @@ func (t *ToolPressButton) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var pressButtonReq option.PressButtonRequest - if err := mapToStruct(request.Params.Arguments, &pressButtonReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Press button action logic - log.Info().Str("button", string(pressButtonReq.Button)).Msg("pressing button") - err = driverExt.PressButton(types.DeviceButton(pressButtonReq.Button)) + log.Info().Str("button", string(unifiedReq.Button)).Msg("pressing button") + err = driverExt.PressButton(types.DeviceButton(unifiedReq.Button)) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Press button failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully pressed button: %s", pressButtonReq.Button)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully pressed button: %s", unifiedReq.Button)), nil } } @@ -980,35 +967,35 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var swipeReq option.SwipeRequest - if err := mapToStruct(request.Params.Arguments, &swipeReq); err != nil { + var unifiedReq option.UnifiedActionRequest + 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", swipeReq.Direction).Msg("performing swipe") + log.Info().Str("direction", unifiedReq.Direction).Msg("performing swipe") // Validate direction validDirections := []string{"up", "down", "left", "right"} isValid := false for _, validDir := range validDirections { - if swipeReq.Direction == validDir { + if unifiedReq.Direction == validDir { isValid = true break } } if !isValid { - return nil, fmt.Errorf("invalid swipe direction: %s, expected one of: %v", swipeReq.Direction, validDirections) + return nil, fmt.Errorf("invalid swipe direction: %s, expected one of: %v", unifiedReq.Direction, validDirections) } opts := []option.ActionOption{ option.WithPreMarkOperation(true), - option.WithDuration(swipeReq.Duration), - option.WithPressDuration(swipeReq.PressDuration), + option.WithDuration(getFloat64ValueOrDefault(unifiedReq.Duration, 0.5)), + option.WithPressDuration(getFloat64ValueOrDefault(unifiedReq.PressDuration, 0.1)), } // Convert direction to coordinates and perform swipe - switch swipeReq.Direction { + switch unifiedReq.Direction { case "up": err = driverExt.Swipe(0.5, 0.5, 0.5, 0.1, opts...) case "down": @@ -1018,14 +1005,14 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc { case "right": err = driverExt.Swipe(0.5, 0.5, 0.9, 0.5, opts...) default: - return mcp.NewToolResultError(fmt.Sprintf("Unexpected swipe direction: %s", swipeReq.Direction)), nil + return mcp.NewToolResultError(fmt.Sprintf("Unexpected swipe direction: %s", unifiedReq.Direction)), nil } if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Swipe failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully swiped %s", swipeReq.Direction)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully swiped %s", unifiedReq.Direction)), nil } } @@ -1070,24 +1057,29 @@ func (t *ToolSwipeCoordinate) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var swipeAdvReq option.SwipeAdvancedRequest - if err := mapToStruct(request.Params.Arguments, &swipeAdvReq); err != nil { + var unifiedReq option.UnifiedActionRequest + 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 { + 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", swipeAdvReq.FromX).Float64("fromY", swipeAdvReq.FromY). - Float64("toX", swipeAdvReq.ToX).Float64("toY", swipeAdvReq.ToY). + Float64("fromX", *unifiedReq.FromX).Float64("fromY", *unifiedReq.FromY). + Float64("toX", *unifiedReq.ToX).Float64("toY", *unifiedReq.ToY). Msg("performing advanced swipe") - params := []float64{swipeAdvReq.FromX, swipeAdvReq.FromY, swipeAdvReq.ToX, swipeAdvReq.ToY} + params := []float64{*unifiedReq.FromX, *unifiedReq.FromY, *unifiedReq.ToX, *unifiedReq.ToY} opts := []option.ActionOption{} - if swipeAdvReq.Duration > 0 { - opts = append(opts, option.WithDuration(swipeAdvReq.Duration)) + if unifiedReq.Duration != nil && *unifiedReq.Duration > 0 { + opts = append(opts, option.WithDuration(*unifiedReq.Duration)) } - if swipeAdvReq.PressDuration > 0 { - opts = append(opts, option.WithPressDuration(swipeAdvReq.PressDuration)) + if unifiedReq.PressDuration != nil && *unifiedReq.PressDuration > 0 { + opts = append(opts, option.WithPressDuration(*unifiedReq.PressDuration)) } swipeAction := prepareSwipeAction(driverExt, params, opts...) @@ -1097,7 +1089,7 @@ func (t *ToolSwipeCoordinate) Implement() server.ToolHandlerFunc { } return mcp.NewToolResultText(fmt.Sprintf("Successfully performed advanced swipe from (%.2f, %.2f) to (%.2f, %.2f)", - swipeAdvReq.FromX, swipeAdvReq.FromY, swipeAdvReq.ToX, swipeAdvReq.ToY)), nil + *unifiedReq.FromX, *unifiedReq.FromY, *unifiedReq.ToX, *unifiedReq.ToY)), nil } } @@ -1144,8 +1136,8 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var swipeAppReq option.SwipeToTapAppRequest - if err := mapToStruct(request.Params.Arguments, &swipeAppReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1153,26 +1145,26 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc { var opts []option.ActionOption // Add boolean options - if swipeAppReq.IgnoreNotFoundError { + if getBoolValue(unifiedReq.IgnoreNotFoundError) { opts = append(opts, option.WithIgnoreNotFoundError(true)) } // Add numeric options - if swipeAppReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(swipeAppReq.MaxRetryTimes)) + if unifiedReq.MaxRetryTimes != nil && *unifiedReq.MaxRetryTimes > 0 { + opts = append(opts, option.WithMaxRetryTimes(*unifiedReq.MaxRetryTimes)) } - if swipeAppReq.Index > 0 { - opts = append(opts, option.WithIndex(swipeAppReq.Index)) + if unifiedReq.Index != nil && *unifiedReq.Index > 0 { + opts = append(opts, option.WithIndex(*unifiedReq.Index)) } // Swipe to tap app action logic - log.Info().Str("appName", swipeAppReq.AppName).Msg("swipe to tap app") - err = driverExt.SwipeToTapApp(swipeAppReq.AppName, opts...) + log.Info().Str("appName", unifiedReq.AppName).Msg("swipe to tap app") + err = driverExt.SwipeToTapApp(unifiedReq.AppName, opts...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap app failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully found and tapped app: %s", swipeAppReq.AppName)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully found and tapped app: %s", unifiedReq.AppName)), nil } } @@ -1213,8 +1205,8 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var swipeTextReq option.SwipeToTapTextRequest - if err := mapToStruct(request.Params.Arguments, &swipeTextReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1222,29 +1214,29 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc { var opts []option.ActionOption // Add boolean options - if swipeTextReq.IgnoreNotFoundError { + if getBoolValue(unifiedReq.IgnoreNotFoundError) { opts = append(opts, option.WithIgnoreNotFoundError(true)) } - if swipeTextReq.Regex { + if getBoolValue(unifiedReq.Regex) { opts = append(opts, option.WithRegex(true)) } // Add numeric options - if swipeTextReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(swipeTextReq.MaxRetryTimes)) + if unifiedReq.MaxRetryTimes != nil && *unifiedReq.MaxRetryTimes > 0 { + opts = append(opts, option.WithMaxRetryTimes(*unifiedReq.MaxRetryTimes)) } - if swipeTextReq.Index > 0 { - opts = append(opts, option.WithIndex(swipeTextReq.Index)) + if unifiedReq.Index != nil && *unifiedReq.Index > 0 { + opts = append(opts, option.WithIndex(*unifiedReq.Index)) } // Swipe to tap text action logic - log.Info().Str("text", swipeTextReq.Text).Msg("swipe to tap text") - err = driverExt.SwipeToTapTexts([]string{swipeTextReq.Text}, opts...) + log.Info().Str("text", unifiedReq.Text).Msg("swipe to tap text") + err = driverExt.SwipeToTapTexts([]string{unifiedReq.Text}, opts...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap text failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully found and tapped text: %s", swipeTextReq.Text)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully found and tapped text: %s", unifiedReq.Text)), nil } } @@ -1285,8 +1277,8 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var swipeTextsReq option.SwipeToTapTextsRequest - if err := mapToStruct(request.Params.Arguments, &swipeTextsReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } @@ -1294,29 +1286,29 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc { var opts []option.ActionOption // Add boolean options - if swipeTextsReq.IgnoreNotFoundError { + if getBoolValue(unifiedReq.IgnoreNotFoundError) { opts = append(opts, option.WithIgnoreNotFoundError(true)) } - if swipeTextsReq.Regex { + if getBoolValue(unifiedReq.Regex) { opts = append(opts, option.WithRegex(true)) } // Add numeric options - if swipeTextsReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(swipeTextsReq.MaxRetryTimes)) + if unifiedReq.MaxRetryTimes != nil && *unifiedReq.MaxRetryTimes > 0 { + opts = append(opts, option.WithMaxRetryTimes(*unifiedReq.MaxRetryTimes)) } - if swipeTextsReq.Index > 0 { - opts = append(opts, option.WithIndex(swipeTextsReq.Index)) + if unifiedReq.Index != nil && *unifiedReq.Index > 0 { + opts = append(opts, option.WithIndex(*unifiedReq.Index)) } // Swipe to tap texts action logic - log.Info().Strs("texts", swipeTextsReq.Texts).Msg("swipe to tap texts") - err = driverExt.SwipeToTapTexts(swipeTextsReq.Texts, opts...) + log.Info().Strs("texts", unifiedReq.Texts).Msg("swipe to tap texts") + err = driverExt.SwipeToTapTexts(unifiedReq.Texts, opts...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Swipe to tap texts failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully found and tapped one of texts: %v", swipeTextsReq.Texts)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully found and tapped one of texts: %v", unifiedReq.Texts)), nil } } @@ -1362,29 +1354,34 @@ func (t *ToolDrag) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var dragReq option.DragRequest - if err := mapToStruct(request.Params.Arguments, &dragReq); err != nil { + var unifiedReq option.UnifiedActionRequest + 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 { + return nil, fmt.Errorf("fromX, fromY, toX, and toY coordinates are required") + } + opts := []option.ActionOption{} - if dragReq.Duration > 0 { - opts = append(opts, option.WithDuration(dragReq.Duration/1000.0)) + if unifiedReq.Duration != nil && *unifiedReq.Duration > 0 { + opts = append(opts, option.WithDuration(*unifiedReq.Duration/1000.0)) } // Drag action logic log.Info(). - Float64("fromX", dragReq.FromX).Float64("fromY", dragReq.FromY). - Float64("toX", dragReq.ToX).Float64("toY", dragReq.ToY). + Float64("fromX", *unifiedReq.FromX).Float64("fromY", *unifiedReq.FromY). + Float64("toX", *unifiedReq.ToX).Float64("toY", *unifiedReq.ToY). Msg("performing drag") - err = driverExt.Swipe(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.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)", - dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY)), nil + *unifiedReq.FromX, *unifiedReq.FromY, *unifiedReq.ToX, *unifiedReq.ToY)), nil } } @@ -1555,23 +1552,23 @@ func (t *ToolInput) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var inputReq option.InputRequest - if err := mapToStruct(request.Params.Arguments, &inputReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - if inputReq.Text == "" { + if unifiedReq.Text == "" { return nil, fmt.Errorf("text is required") } // Input action logic - log.Info().Str("text", inputReq.Text).Msg("inputting text") - err = driverExt.Input(inputReq.Text) + log.Info().Str("text", unifiedReq.Text).Msg("inputting text") + err = driverExt.Input(unifiedReq.Text) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Input failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully input text: %s", inputReq.Text)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully input text: %s", unifiedReq.Text)), nil } } @@ -1606,19 +1603,19 @@ func (t *ToolWebLoginNoneUI) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var loginReq option.WebLoginNoneUIRequest - if err := mapToStruct(request.Params.Arguments, &loginReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Web login none UI action logic - log.Info().Str("packageName", loginReq.PackageName).Msg("performing web login without UI") + log.Info().Str("packageName", unifiedReq.PackageName).Msg("performing web login without UI") driver, ok := driverExt.IDriver.(*BrowserDriver) if !ok { return nil, fmt.Errorf("invalid browser driver for web login") } - _, err = driver.LoginNoneUI(loginReq.PackageName, loginReq.PhoneNumber, loginReq.Captcha, loginReq.Password) + _, err = driver.LoginNoneUI(unifiedReq.PackageName, unifiedReq.PhoneNumber, unifiedReq.Captcha, unifiedReq.Password) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Web login failed: %s", err.Error())), nil } @@ -1654,19 +1651,19 @@ func (t *ToolAppInstall) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var installReq option.AppInstallRequest - if err := mapToStruct(request.Params.Arguments, &installReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // App install action logic - log.Info().Str("appUrl", installReq.AppUrl).Msg("installing app") - err = driverExt.GetDevice().Install(installReq.AppUrl) + log.Info().Str("appUrl", unifiedReq.AppUrl).Msg("installing app") + err = driverExt.GetDevice().Install(unifiedReq.AppUrl) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("App install failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully installed app from: %s", installReq.AppUrl)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully installed app from: %s", unifiedReq.AppUrl)), nil } } @@ -1703,19 +1700,19 @@ func (t *ToolAppUninstall) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var uninstallReq option.AppUninstallRequest - if err := mapToStruct(request.Params.Arguments, &uninstallReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // App uninstall action logic - log.Info().Str("packageName", uninstallReq.PackageName).Msg("uninstalling app") - err = driverExt.GetDevice().Uninstall(uninstallReq.PackageName) + log.Info().Str("packageName", unifiedReq.PackageName).Msg("uninstalling app") + err = driverExt.GetDevice().Uninstall(unifiedReq.PackageName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("App uninstall failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully uninstalled app: %s", uninstallReq.PackageName)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully uninstalled app: %s", unifiedReq.PackageName)), nil } } @@ -1752,19 +1749,19 @@ func (t *ToolAppClear) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var clearReq option.AppClearRequest - if err := mapToStruct(request.Params.Arguments, &clearReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // App clear action logic - log.Info().Str("packageName", clearReq.PackageName).Msg("clearing app") - err = driverExt.AppClear(clearReq.PackageName) + log.Info().Str("packageName", unifiedReq.PackageName).Msg("clearing app") + err = driverExt.AppClear(unifiedReq.PackageName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("App clear failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully cleared app: %s", clearReq.PackageName)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully cleared app: %s", unifiedReq.PackageName)), nil } } @@ -1801,19 +1798,24 @@ func (t *ToolSecondaryClick) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var clickReq option.SecondaryClickRequest - if err := mapToStruct(request.Params.Arguments, &clickReq); err != nil { + var unifiedReq option.UnifiedActionRequest + 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 { + return nil, fmt.Errorf("x and y coordinates are required") + } + // Secondary click action logic - log.Info().Float64("x", clickReq.X).Float64("y", clickReq.Y).Msg("performing secondary click") - err = driverExt.SecondaryClick(clickReq.X, clickReq.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)", clickReq.X, clickReq.Y)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully performed secondary click at (%.2f, %.2f)", *unifiedReq.X, *unifiedReq.Y)), nil } } @@ -1851,19 +1853,19 @@ func (t *ToolHoverBySelector) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var selectorReq option.SelectorRequest - if err := mapToStruct(request.Params.Arguments, &selectorReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Hover by selector action logic - log.Info().Str("selector", selectorReq.Selector).Msg("hovering by selector") - err = driverExt.HoverBySelector(selectorReq.Selector) + log.Info().Str("selector", unifiedReq.Selector).Msg("hovering by selector") + err = driverExt.HoverBySelector(unifiedReq.Selector) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Hover by selector failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully hovered over element with selector: %s", selectorReq.Selector)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully hovered over element with selector: %s", unifiedReq.Selector)), nil } } @@ -1900,19 +1902,19 @@ func (t *ToolTapBySelector) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var selectorReq option.SelectorRequest - if err := mapToStruct(request.Params.Arguments, &selectorReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Tap by selector action logic - log.Info().Str("selector", selectorReq.Selector).Msg("tapping by selector") - err = driverExt.TapBySelector(selectorReq.Selector) + log.Info().Str("selector", unifiedReq.Selector).Msg("tapping by selector") + err = driverExt.TapBySelector(unifiedReq.Selector) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Tap by selector failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped element with selector: %s", selectorReq.Selector)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully tapped element with selector: %s", unifiedReq.Selector)), nil } } @@ -1949,19 +1951,19 @@ func (t *ToolSecondaryClickBySelector) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var selectorReq option.SelectorRequest - if err := mapToStruct(request.Params.Arguments, &selectorReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Secondary click by selector action logic - log.Info().Str("selector", selectorReq.Selector).Msg("performing secondary click by selector") - err = driverExt.SecondaryClickBySelector(selectorReq.Selector) + log.Info().Str("selector", unifiedReq.Selector).Msg("performing secondary click by selector") + err = driverExt.SecondaryClickBySelector(unifiedReq.Selector) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Secondary click by selector failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully performed secondary click on element with selector: %s", selectorReq.Selector)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully performed secondary click on element with selector: %s", unifiedReq.Selector)), nil } } @@ -1998,24 +2000,29 @@ func (t *ToolWebCloseTab) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var closeTabReq option.WebCloseTabRequest - if err := mapToStruct(request.Params.Arguments, &closeTabReq); err != nil { + var unifiedReq option.UnifiedActionRequest + 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 { + return nil, fmt.Errorf("tabIndex is required") + } + // Web close tab action logic - log.Info().Int("tabIndex", closeTabReq.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(closeTabReq.TabIndex) + err = browserDriver.CloseTab(*unifiedReq.TabIndex) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Close tab failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully closed tab at index: %d", closeTabReq.TabIndex)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully closed tab at index: %d", unifiedReq.TabIndex)), nil } } @@ -2060,19 +2067,19 @@ func (t *ToolSetIme) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var imeReq option.SetImeRequest - if err := mapToStruct(request.Params.Arguments, &imeReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Set IME action logic - log.Info().Str("ime", imeReq.Ime).Msg("setting IME") - err = driverExt.SetIme(imeReq.Ime) + log.Info().Str("ime", unifiedReq.Ime).Msg("setting IME") + err = driverExt.SetIme(unifiedReq.Ime) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Set IME failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully set IME to: %s", imeReq.Ime)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully set IME to: %s", unifiedReq.Ime)), nil } } @@ -2109,19 +2116,19 @@ func (t *ToolGetSource) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var sourceReq option.GetSourceRequest - if err := mapToStruct(request.Params.Arguments, &sourceReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Get source action logic - log.Info().Str("packageName", sourceReq.PackageName).Msg("getting source") - _, err = driverExt.Source(option.WithProcessName(sourceReq.PackageName)) + log.Info().Str("packageName", unifiedReq.PackageName).Msg("getting source") + _, err = driverExt.Source(option.WithProcessName(unifiedReq.PackageName)) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Get source failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully retrieved source for package: %s", sourceReq.PackageName)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully retrieved source for package: %s", unifiedReq.PackageName)), nil } } @@ -2211,16 +2218,21 @@ func (t *ToolSleepMS) Options() []mcp.ToolOption { func (t *ToolSleepMS) Implement() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - var sleepReq option.SleepMSRequest - if err := mapToStruct(request.Params.Arguments, &sleepReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - // Sleep MS action logic - log.Info().Int64("milliseconds", sleepReq.Milliseconds).Msg("sleeping in milliseconds") - time.Sleep(time.Duration(sleepReq.Milliseconds) * time.Millisecond) + // Validate required parameters + if unifiedReq.Milliseconds == nil { + return nil, fmt.Errorf("milliseconds is required") + } - return mcp.NewToolResultText(fmt.Sprintf("Successfully slept for %d milliseconds", sleepReq.Milliseconds)), nil + // Sleep MS action logic + 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 } } @@ -2257,16 +2269,16 @@ func (t *ToolSleepRandom) Options() []mcp.ToolOption { func (t *ToolSleepRandom) Implement() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - var sleepRandomReq option.SleepRandomRequest - if err := mapToStruct(request.Params.Arguments, &sleepRandomReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // Sleep random action logic - log.Info().Floats64("params", sleepRandomReq.Params).Msg("sleeping for random duration") - sleepStrict(time.Now(), getSimulationDuration(sleepRandomReq.Params)) + log.Info().Floats64("params", unifiedReq.Params).Msg("sleeping for random duration") + sleepStrict(time.Now(), getSimulationDuration(unifiedReq.Params)) - return mcp.NewToolResultText(fmt.Sprintf("Successfully slept for random duration with params: %v", sleepRandomReq.Params)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully slept for random duration with params: %v", unifiedReq.Params)), nil } } @@ -2341,19 +2353,19 @@ func (t *ToolAIAction) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("setup driver failed: %w", err) } - var aiReq option.AIActionRequest - if err := mapToStruct(request.Params.Arguments, &aiReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } // AI action logic - log.Info().Str("prompt", aiReq.Prompt).Msg("performing AI action") - err = driverExt.AIAction(aiReq.Prompt) + log.Info().Str("prompt", unifiedReq.Prompt).Msg("performing AI action") + err = driverExt.AIAction(unifiedReq.Prompt) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("AI action failed: %s", err.Error())), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully performed AI action with prompt: %s", aiReq.Prompt)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully performed AI action with prompt: %s", unifiedReq.Prompt)), nil } } @@ -2385,13 +2397,13 @@ func (t *ToolFinished) Options() []mcp.ToolOption { func (t *ToolFinished) Implement() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - var finishedReq option.FinishedRequest - if err := mapToStruct(request.Params.Arguments, &finishedReq); err != nil { + var unifiedReq option.UnifiedActionRequest + if err := mapToStruct(request.Params.Arguments, &unifiedReq); err != nil { return nil, fmt.Errorf("parse parameters error: %w", err) } - log.Info().Str("reason", finishedReq.Content).Msg("task finished") + log.Info().Str("reason", unifiedReq.Content).Msg("task finished") - return mcp.NewToolResultText(fmt.Sprintf("Task completed: %s", finishedReq.Content)), nil + return mcp.NewToolResultText(fmt.Sprintf("Task completed: %s", unifiedReq.Content)), nil } } @@ -2404,3 +2416,32 @@ func (t *ToolFinished) ConvertActionToCallToolRequest(action MobileAction) (mcp. } return mcp.CallToolRequest{}, fmt.Errorf("invalid finished params: %v", action.Params) } + +// Helper functions for pointer type handling +func getFloat64Value(ptr *float64) float64 { + if ptr == nil { + return 0 + } + return *ptr +} + +func getFloat64ValueOrDefault(ptr *float64, defaultValue float64) float64 { + if ptr == nil { + return defaultValue + } + return *ptr +} + +func getIntValue(ptr *int) int { + if ptr == nil { + return 0 + } + return *ptr +} + +func getBoolValue(ptr *bool) bool { + if ptr == nil { + return false + } + return *ptr +} diff --git a/uixt/option/action.go b/uixt/option/action.go index 75385178..9ebd3d30 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -32,10 +32,12 @@ const ( // 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) @@ -45,13 +47,22 @@ const ( 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 // device actions ACTION_ListAvailableDevices ActionMethod = "list_available_devices" diff --git a/uixt/option/migration_summary.md b/uixt/option/migration_summary.md deleted file mode 100644 index 8d1c8b69..00000000 --- a/uixt/option/migration_summary.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/uixt/option/request.go b/uixt/option/request.go index d80d3e37..ae8011d6 100644 --- a/uixt/option/request.go +++ b/uixt/option/request.go @@ -1,6 +1,8 @@ package option import ( + "context" + "fmt" "reflect" "strings" @@ -9,218 +11,9 @@ import ( "github.com/rs/zerolog/log" ) -type TargetDeviceRequest struct { - Platform string `json:"platform" binding:"required" desc:"Device platform: android/ios/browser"` - Serial string `json:"serial" binding:"required" desc:"Device serial/udid/browser id"` -} - -type TapRequest struct { - TargetDeviceRequest - X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"` - Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"` - Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"` -} - -type DragRequest struct { - TargetDeviceRequest - FromX float64 `json:"from_x" binding:"required" desc:"Starting X-coordinate (percentage, 0.0 to 1.0)"` - FromY float64 `json:"from_y" binding:"required" desc:"Starting Y-coordinate (percentage, 0.0 to 1.0)"` - ToX float64 `json:"to_x" binding:"required" desc:"Ending X-coordinate (percentage, 0.0 to 1.0)"` - ToY float64 `json:"to_y" binding:"required" desc:"Ending Y-coordinate (percentage, 0.0 to 1.0)"` - Duration float64 `json:"duration" desc:"Swipe duration in milliseconds (optional)"` - PressDuration float64 `json:"press_duration" desc:"Press duration in milliseconds (optional)"` -} - -type SwipeRequest struct { - TargetDeviceRequest - Direction string `json:"direction" binding:"required" desc:"The direction of the swipe. Supported directions: up, down, left, right"` - Duration float64 `json:"duration" desc:"Swipe duration in milliseconds (optional)"` - PressDuration float64 `json:"press_duration" desc:"Press duration in milliseconds (optional)"` -} - -type InputRequest struct { - TargetDeviceRequest - Text string `json:"text" binding:"required"` - Frequency int `json:"frequency"` // only iOS -} - -type DeleteRequest struct { - TargetDeviceRequest - Count int `json:"count" binding:"required"` -} - -type KeycodeRequest struct { - TargetDeviceRequest - Keycode int `json:"keycode" binding:"required"` -} - -type AppInstallRequest struct { - TargetDeviceRequest - AppUrl string `json:"appUrl" binding:"required"` - MappingUrl string `json:"mappingUrl"` - ResourceMappingUrl string `json:"resourceMappingUrl"` - PackageName string `json:"packageName"` -} - -type AppInfoRequest struct { - TargetDeviceRequest - PackageName string `form:"packageName" binding:"required"` -} - -type AppUninstallRequest struct { - TargetDeviceRequest - PackageName string `json:"packageName" binding:"required"` -} - -type AppClearRequest struct { - TargetDeviceRequest - PackageName string `json:"packageName" binding:"required"` -} - -type AppLaunchRequest struct { - TargetDeviceRequest - PackageName string `json:"packageName" binding:"required" desc:"The package name of the app to launch"` -} - -type AppTerminateRequest struct { - TargetDeviceRequest - PackageName string `json:"packageName" binding:"required" desc:"The package name of the app to terminate"` -} - -type PressButtonRequest struct { - TargetDeviceRequest - Button types.DeviceButton `json:"button" binding:"required" desc:"The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER."` -} - -// Additional requests for missing actions -type WebLoginNoneUIRequest struct { - TargetDeviceRequest - PackageName string `json:"packageName" binding:"required" desc:"Package name for the app to login"` - PhoneNumber string `json:"phoneNumber" binding:"required" desc:"Phone number for login"` - Captcha string `json:"captcha" binding:"required" desc:"Captcha code"` - Password string `json:"password" binding:"required" desc:"Password for login"` -} - -type SwipeToTapAppRequest struct { - TargetDeviceRequest - AppName string `json:"appName" binding:"required" desc:"App name to find and tap"` - IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"` - MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the app"` - Index int `json:"index" desc:"Index of the target element when multiple matches found"` -} - -type SwipeToTapTextRequest struct { - TargetDeviceRequest - Text string `json:"text" binding:"required" desc:"Text to find and tap"` - IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"` - MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the text"` - Index int `json:"index" desc:"Index of the target element when multiple matches found"` - Regex bool `json:"regex" desc:"Use regex to match text"` -} - -type SwipeToTapTextsRequest struct { - TargetDeviceRequest - Texts []string `json:"texts" binding:"required" desc:"List of texts to find and tap"` - IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"` - MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the texts"` - Index int `json:"index" desc:"Index of the target element when multiple matches found"` - Regex bool `json:"regex" desc:"Use regex to match text"` -} - -type SecondaryClickRequest struct { - TargetDeviceRequest - X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"` - Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"` -} - -type SelectorRequest struct { - TargetDeviceRequest - Selector string `json:"selector" binding:"required" desc:"CSS or XPath selector"` -} - -type WebCloseTabRequest struct { - TargetDeviceRequest - TabIndex int `json:"tabIndex" binding:"required" desc:"Index of the tab to close"` -} - -type SetImeRequest struct { - TargetDeviceRequest - Ime string `json:"ime" binding:"required" desc:"IME package name to set"` -} - -type GetSourceRequest struct { - TargetDeviceRequest - PackageName string `json:"packageName" binding:"required" desc:"Package name to get source from"` -} - -type TapAbsXYRequest struct { - TargetDeviceRequest - X float64 `json:"x" binding:"required" desc:"Absolute X coordinate in pixels"` - Y float64 `json:"y" binding:"required" desc:"Absolute Y coordinate in pixels"` - Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"` -} - -type TapByOCRRequest struct { - TargetDeviceRequest - Text string `json:"text" binding:"required" desc:"OCR text to find and tap"` - IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"` - MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the text"` - Index int `json:"index" desc:"Index of the target element when multiple matches found"` - Regex bool `json:"regex" desc:"Use regex to match text"` - TapRandomRect bool `json:"tap_random_rect" desc:"Tap random point in text rectangle"` -} - -type TapByCVRequest struct { - TargetDeviceRequest - ImagePath string `json:"imagePath" desc:"Path to reference image for CV recognition"` - IgnoreNotFoundError bool `json:"ignore_NotFoundError" desc:"Ignore error if target element not found"` - MaxRetryTimes int `json:"max_retry_times" desc:"Maximum retry times for finding the image"` - Index int `json:"index" desc:"Index of the target element when multiple matches found"` - TapRandomRect bool `json:"tap_random_rect" desc:"Tap random point in image rectangle"` -} - -type DoubleTapXYRequest struct { - TargetDeviceRequest - X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"` - Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"` -} - -type SwipeAdvancedRequest struct { - TargetDeviceRequest - FromX float64 `json:"fromX" binding:"required" desc:"Starting X coordinate"` - FromY float64 `json:"fromY" binding:"required" desc:"Starting Y coordinate"` - ToX float64 `json:"toX" binding:"required" desc:"Ending X coordinate"` - ToY float64 `json:"toY" binding:"required" desc:"Ending Y coordinate"` - Duration float64 `json:"duration" desc:"Swipe duration in seconds (optional)"` - PressDuration float64 `json:"pressDuration" desc:"Press duration in seconds (optional)"` -} - -type SleepMSRequest struct { - TargetDeviceRequest - Milliseconds int64 `json:"milliseconds" binding:"required" desc:"Sleep duration in milliseconds"` -} - -type SleepRandomRequest struct { - TargetDeviceRequest - Params []float64 `json:"params" binding:"required" desc:"Random sleep parameters [min, max] or [min1, max1, weight1, ...]"` -} - -type CallFunctionRequest struct { - TargetDeviceRequest - Description string `json:"description" binding:"required" desc:"Function description"` -} - -type AIActionRequest struct { - TargetDeviceRequest - Prompt string `json:"prompt" binding:"required" desc:"AI action prompt"` -} - -type FinishedRequest struct { - Content string `json:"content" binding:"required" desc:"Completion message for finished reason"` -} - -// NewMCPOptions generates mcp.NewTool parameters from a struct type. -// It automatically generates mcp.NewTool parameters based on the struct fields and their desc tags. +// 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) @@ -245,7 +38,13 @@ func NewMCPOptions(t interface{}) (options []mcp.ToolOption) { binding := field.Tag.Get("binding") required := strings.Contains(binding, "required") desc := field.Tag.Get("desc") - switch field.Type.Kind() { + 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))) @@ -287,3 +86,667 @@ func NewMCPOptions(t interface{}) (options []mcp.ToolOption) { } 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/unified_request_test.go b/uixt/option/request_test.go similarity index 59% rename from uixt/option/unified_request_test.go rename to uixt/option/request_test.go index 972e2f19..10dad159 100644 --- a/uixt/option/unified_request_test.go +++ b/uixt/option/request_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestUnifiedActionRequest_ToActionOptions(t *testing.T) { @@ -101,78 +100,6 @@ func TestUnifiedActionRequest_ScreenOptions(t *testing.T) { assert.Equal(t, uiTypes, actionOpts.ScreenShotWithUITypes) } -func TestMigrationHelpers(t *testing.T) { - // Test TapRequest migration - oldTapReq := TapRequest{ - TargetDeviceRequest: TargetDeviceRequest{ - Platform: "android", - Serial: "device123", - }, - X: 0.5, - Y: 0.7, - Duration: 1.0, - } - - unifiedReq := MigrateTapRequestToUnified(oldTapReq) - require.NotNil(t, unifiedReq.X) - require.NotNil(t, unifiedReq.Y) - require.NotNil(t, unifiedReq.Duration) - assert.Equal(t, 0.5, *unifiedReq.X) - assert.Equal(t, 0.7, *unifiedReq.Y) - assert.Equal(t, 1.0, *unifiedReq.Duration) - assert.Equal(t, "android", unifiedReq.Platform) - assert.Equal(t, "device123", unifiedReq.Serial) - - // Test SwipeRequest migration - oldSwipeReq := SwipeRequest{ - TargetDeviceRequest: TargetDeviceRequest{ - Platform: "ios", - Serial: "device456", - }, - Direction: "up", - Duration: 2.0, - PressDuration: 0.5, - } - - unifiedSwipeReq := MigrateSwipeRequestToUnified(oldSwipeReq) - require.NotNil(t, unifiedSwipeReq.Duration) - require.NotNil(t, unifiedSwipeReq.PressDuration) - assert.Equal(t, "up", unifiedSwipeReq.Direction) - assert.Equal(t, 2.0, *unifiedSwipeReq.Duration) - assert.Equal(t, 0.5, *unifiedSwipeReq.PressDuration) - assert.Equal(t, "ios", unifiedSwipeReq.Platform) - assert.Equal(t, "device456", unifiedSwipeReq.Serial) - - // Test TapByOCRRequest migration - oldOCRReq := TapByOCRRequest{ - TargetDeviceRequest: TargetDeviceRequest{ - Platform: "android", - Serial: "device789", - }, - Text: "登录", - IgnoreNotFoundError: true, - MaxRetryTimes: 3, - Index: 1, - Regex: true, - TapRandomRect: false, - } - - unifiedOCRReq := MigrateTapByOCRRequestToUnified(oldOCRReq) - require.NotNil(t, unifiedOCRReq.IgnoreNotFoundError) - require.NotNil(t, unifiedOCRReq.MaxRetryTimes) - require.NotNil(t, unifiedOCRReq.Index) - require.NotNil(t, unifiedOCRReq.Regex) - require.NotNil(t, unifiedOCRReq.TapRandomRect) - assert.Equal(t, "登录", unifiedOCRReq.Text) - assert.True(t, *unifiedOCRReq.IgnoreNotFoundError) - assert.Equal(t, 3, *unifiedOCRReq.MaxRetryTimes) - assert.Equal(t, 1, *unifiedOCRReq.Index) - assert.True(t, *unifiedOCRReq.Regex) - assert.False(t, *unifiedOCRReq.TapRandomRect) - assert.Equal(t, "android", unifiedOCRReq.Platform) - assert.Equal(t, "device789", unifiedOCRReq.Serial) -} - func TestUnifiedActionRequest_NilPointerSafety(t *testing.T) { // Test with nil pointers unifiedReq := &UnifiedActionRequest{ diff --git a/uixt/option/unified_request.go b/uixt/option/unified_request.go deleted file mode 100644 index 441a8e77..00000000 --- a/uixt/option/unified_request.go +++ /dev/null @@ -1,350 +0,0 @@ -package option - -import ( - "context" - "reflect" - "strings" - - "github.com/httprunner/httprunner/v5/uixt/types" - "github.com/mark3labs/mcp-go/mcp" - "github.com/rs/zerolog/log" -) - -// UnifiedActionRequest represents a unified request structure that combines -// ActionOptions with specific action parameters -type UnifiedActionRequest struct { - // Device targeting - Platform string `json:"platform" binding:"required" desc:"Device platform: android/ios/browser"` - Serial string `json:"serial" binding:"required" desc:"Device serial/udid/browser id"` - - // Common action parameters - X *float64 `json:"x,omitempty" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"` - Y *float64 `json:"y,omitempty" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"` - FromX *float64 `json:"fromX,omitempty" desc:"Starting X coordinate"` - FromY *float64 `json:"fromY,omitempty" desc:"Starting Y coordinate"` - ToX *float64 `json:"toX,omitempty" desc:"Ending X coordinate"` - ToY *float64 `json:"toY,omitempty" 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"` - - // 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"` - - // 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:"maxRetryTimes,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:"pressDuration,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:"tapOffset,omitempty" desc:"Tap offset [x,y]"` - TapRandomRect *bool `json:"tapRandomRect,omitempty" desc:"Tap random point in rectangle"` - SwipeOffset []int `json:"swipeOffset,omitempty" desc:"Swipe offset [fromX,fromY,toX,toY]"` - OffsetRandomRange []int `json:"offsetRandomRange,omitempty" desc:"Random offset range [min,max]"` - Index *int `json:"index,omitempty" desc:"Element index when multiple matches found"` - MatchOne *bool `json:"matchOne,omitempty" desc:"Match only one element"` - IgnoreNotFoundError *bool `json:"ignoreNotFoundError,omitempty" desc:"Ignore error if element not found"` - - // Screenshot options (from ScreenShotOptions) - ScreenShotWithOCR *bool `json:"screenshotWithOCR,omitempty" desc:"Take screenshot with OCR"` - ScreenShotWithUpload *bool `json:"screenshotWithUpload,omitempty" desc:"Upload screenshot"` - ScreenShotWithLiveType *bool `json:"screenshotWithLiveType,omitempty" desc:"Screenshot with live type"` - ScreenShotWithLivePopularity *bool `json:"screenshotWithLivePopularity,omitempty" desc:"Screenshot with live popularity"` - ScreenShotWithUITypes []string `json:"screenshotWithUITypes,omitempty" desc:"Screenshot with UI types"` - ScreenShotWithClosePopups *bool `json:"screenshotWithClosePopups,omitempty" desc:"Close popups before screenshot"` - ScreenShotWithOCRCluster string `json:"screenshotWithOCRCluster,omitempty" desc:"OCR cluster for screenshot"` - ScreenShotFileName string `json:"screenshotFileName,omitempty" desc:"Screenshot file name"` - - // Screen record options (from ScreenRecordOptions) - ScreenRecordDuration *float64 `json:"screenRecordDuration,omitempty" desc:"Screen record duration"` - ScreenRecordWithAudio *bool `json:"screenRecordWithAudio,omitempty" desc:"Record with audio"` - ScreenRecordWithScrcpy *bool `json:"screenRecordWithScrcpy,omitempty" desc:"Use scrcpy for recording"` - ScreenRecordPath string `json:"screenRecordPath,omitempty" desc:"Screen record output path"` - - // Mark operation options (from MarkOperationOptions) - PreMarkOperation *bool `json:"preMarkOperation,omitempty" desc:"Mark operation before action"` - PostMarkOperation *bool `json:"postMarkOperation,omitempty" desc:"Mark operation after action"` - - // Custom options - Custom map[string]interface{} `json:"custom,omitempty" desc:"Custom options"` -} - -// 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 - opts.Scope = r.Scope - opts.AbsScope = r.AbsScope - if r.Regex != nil { - opts.Regex = *r.Regex - } - opts.TapOffset = r.TapOffset - if r.TapRandomRect != nil { - opts.TapRandomRect = *r.TapRandomRect - } - opts.SwipeOffset = r.SwipeOffset - opts.OffsetRandomRange = r.OffsetRandomRange - if r.Index != nil { - opts.Index = *r.Index - } - if r.MatchOne != nil { - opts.MatchOne = *r.MatchOne - } - if r.IgnoreNotFoundError != nil { - opts.IgnoreNotFoundError = *r.IgnoreNotFoundError - } - - // Copy screenshot options - if r.ScreenShotWithOCR != nil { - opts.ScreenShotWithOCR = *r.ScreenShotWithOCR - } - if r.ScreenShotWithUpload != nil { - opts.ScreenShotWithUpload = *r.ScreenShotWithUpload - } - if r.ScreenShotWithLiveType != nil { - opts.ScreenShotWithLiveType = *r.ScreenShotWithLiveType - } - if r.ScreenShotWithLivePopularity != nil { - opts.ScreenShotWithLivePopularity = *r.ScreenShotWithLivePopularity - } - opts.ScreenShotWithUITypes = r.ScreenShotWithUITypes - if r.ScreenShotWithClosePopups != nil { - opts.ScreenShotWithClosePopups = *r.ScreenShotWithClosePopups - } - opts.ScreenShotWithOCRCluster = r.ScreenShotWithOCRCluster - opts.ScreenShotFileName = r.ScreenShotFileName - - // Copy screen record options - if r.ScreenRecordDuration != nil { - opts.ScreenRecordDuration = *r.ScreenRecordDuration - } - if r.ScreenRecordWithAudio != nil { - opts.ScreenRecordWithAudio = *r.ScreenRecordWithAudio - } - if r.ScreenRecordWithScrcpy != nil { - opts.ScreenRecordWithScrcpy = *r.ScreenRecordWithScrcpy - } - opts.ScreenRecordPath = r.ScreenRecordPath - - // Copy mark operation options - if r.PreMarkOperation != nil { - opts.PreMarkOperation = *r.PreMarkOperation - } - if r.PostMarkOperation != nil { - opts.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 -}