mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-26 01:51:29 +08:00
refactor: improve ActionMethod type safety and eliminate type conversions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
v5.0.0-beta-2505262313
|
||||
v5.0.0-beta-2505271149
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
104
server/ui.go
104
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user