diff --git a/internal/version/VERSION b/internal/version/VERSION index 392a87ad..3c80d5e7 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2506211542 +v5.0.0-beta-2506222254 diff --git a/runner.go b/runner.go index 73ac82b1..34d0d5cb 100644 --- a/runner.go +++ b/runner.go @@ -681,10 +681,10 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCa for _, cached := range uixt.ListCachedDrivers() { // add WDA/UIA logs to summary logs := map[string]interface{}{ - "uuid": cached.Serial, + "uuid": cached.Key, } - client := cached.Driver + client := cached.Item if client.GetDevice().LogEnabled() { log, err1 := client.StopCaptureLog() if err1 != nil { diff --git a/server/app.go b/server/app.go deleted file mode 100644 index 57a5f07b..00000000 --- a/server/app.go +++ /dev/null @@ -1,132 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v5/uixt" - "github.com/httprunner/httprunner/v5/uixt/option" -) - -func (r *Router) foregroundAppHandler(c *gin.Context) { - driver, err := r.GetDriver(c) - if err != nil { - return - } - appInfo, err := driver.ForegroundInfo() - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, appInfo) -} - -func (r *Router) appInfoHandler(c *gin.Context) { - var req option.ActionOptions - 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(req.PackageName) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, appInfo) - return - } else if iOSDevice, ok := device.(*uixt.IOSDevice); ok { - appInfo, err := iOSDevice.GetAppInfo(req.PackageName) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, appInfo) - return - } -} - -func (r *Router) clearAppHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_AppClear) - if err != nil { - return - } - - driver, err := r.GetDriver(c) - if err != nil { - return - } - err = driver.AppClear(req.PackageName) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) launchAppHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_AppLaunch) - if err != nil { - return - } - - driver, err := r.GetDriver(c) - if err != nil { - return - } - err = driver.AppLaunch(req.PackageName) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) terminalAppHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_AppTerminate) - if err != nil { - return - } - - driver, err := r.GetDriver(c) - if err != nil { - return - } - _, err = driver.AppTerminate(req.PackageName) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) uninstallAppHandler(c *gin.Context) { - 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(req.PackageName) - if err != nil { - log.Err(err).Msg("failed to uninstall app") - } - RenderSuccess(c, true) -} diff --git a/server/context.go b/server/context.go index b4f9fd8d..eeb0e964 100644 --- a/server/context.go +++ b/server/context.go @@ -3,6 +3,8 @@ package server import ( "fmt" "net/http" + "strings" + "time" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" @@ -12,10 +14,84 @@ import ( "github.com/httprunner/httprunner/v5/uixt/option" ) -func (r *Router) GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) { - var device uixt.IDevice - var driver uixt.IDriver +func (r *Router) GetDevice(c *gin.Context) (uixt.IDevice, error) { + platform := c.Param("platform") + switch strings.ToLower(platform) { + case "android": + serial := c.Param("serial") + if serial == "" { + err := fmt.Errorf("[%s]: serial is empty", c.HandlerName()) + log.Error().Err(err).Str("platform", platform).Msg(err.Error()) + RenderError(c, err) + return nil, err + } + device, err := uixt.NewAndroidDevice(option.WithSerialNumber(serial)) + if err != nil { + time.Sleep(5 * time.Second) + device, err = uixt.NewAndroidDevice(option.WithSerialNumber(serial)) + if err != nil { + log.Error().Err(err).Str("platform", platform).Str("serial", serial). + Msg(fmt.Sprintf("[%s]: Device Not Found; %s", c.HandlerName(), err.Error())) + RenderErrorInitDevice(c, err) + return nil, err + } + } + c.Set("device", device) + return device, nil + + case "ios": + serial := c.Param("serial") + if serial == "" { + err := fmt.Errorf("[%s]: serial is empty", c.HandlerName()) + log.Error().Err(err).Str("platform", platform).Msg(err.Error()) + RenderError(c, err) + return nil, err + } + device, err := uixt.NewIOSDevice( + option.WithUDID(serial), + option.WithWDAPort(8700), + option.WithWDAMjpegPort(8800), + option.WithResetHomeOnStartup(false)) + if err != nil { + log.Error().Err(err).Str("platform", platform).Str("serial", serial). + Msg(fmt.Sprintf("[%s]: Device Not Found", c.HandlerName())) + RenderErrorInitDevice(c, err) + return nil, err + } + c.Set("device", device) + return device, nil + + case "browser": + serial := c.Param("serial") + if serial == "" { + err := fmt.Errorf("[%s]: serial is empty", c.HandlerName()) + log.Error().Err(err).Str("platform", platform).Msg(err.Error()) + RenderError(c, err) + return nil, err + } + device, err := uixt.NewBrowserDevice(option.WithBrowserID(serial)) + if err != nil { + RenderErrorInitDevice(c, err) + return nil, err + } + c.Set("device", device) + return device, nil + + default: + err := fmt.Errorf("[%s]: invalid platform", c.HandlerName()) + RenderError(c, err) + return nil, err + } +} + +func (r *Router) GetDriver(c *gin.Context) (*uixt.XTDriver, error) { + platform := c.Param("platform") + + // Try to get existing device from context deviceObj, exists := c.Get("device") + var device uixt.IDevice + var err error + if !exists { device, err = r.GetDevice(c) if err != nil { @@ -25,32 +101,24 @@ func (r *Router) GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) device = deviceObj.(uixt.IDevice) } - driver, err = device.NewDriver() + // Create driver + driver, err := device.NewDriver() if err != nil { + log.Error().Err(err).Str("platform", platform).Str("serial", device.UUID()). + Msg(fmt.Sprintf("[%s]: Failed New Driver", c.HandlerName())) RenderErrorInitDriver(c, err) - return + return nil, err } - driverExt, err = uixt.NewXTDriver(driver, - option.WithCVService(option.CVServiceTypeVEDEM)) + // Create XTDriver wrapper + xtDriver, err := uixt.NewXTDriver(driver) if err != nil { RenderErrorInitDriver(c, err) - return + return nil, err } - c.Set("driver", driverExt) - return driverExt, nil -} -func (r *Router) GetDevice(c *gin.Context) (device uixt.IDevice, err error) { - platform := c.Param("platform") - serial := c.Param("serial") - device, err = uixt.NewDeviceWithDefault(platform, serial) - if err != nil { - RenderErrorInitDriver(c, err) - return - } - c.Set("device", device) - return device, nil + c.Set("driver", xtDriver) + return xtDriver, nil } func RenderSuccess(c *gin.Context, result interface{}) { @@ -87,6 +155,21 @@ func RenderErrorInitDriver(c *gin.Context, err error) { c.Abort() } +func RenderErrorInitDevice(c *gin.Context, err error) { + log.Error().Err(err).Msg("init device failed") + errCode := code.GetErrorCode(err) + if errCode == code.GeneralFail { + errCode = code.GetErrorCode(code.DeviceConnectionError) + } + c.JSON(http.StatusInternalServerError, + HttpResponse{ + Code: errCode, + Message: "grey init device failed", + }, + ) + c.Abort() +} + func RenderErrorValidateRequest(c *gin.Context, err error) { log.Error().Err(err).Msg("validate request failed") c.JSON(http.StatusBadRequest, HttpResponse{ diff --git a/server/device.go b/server/device.go deleted file mode 100644 index 549d8703..00000000 --- a/server/device.go +++ /dev/null @@ -1,169 +0,0 @@ -package server - -import ( - "os" - "path" - - "github.com/Masterminds/semver" - "github.com/danielpaulus/go-ios/ios" - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v5/pkg/gadb" - "github.com/httprunner/httprunner/v5/uixt" - "github.com/httprunner/httprunner/v5/uixt/option" -) - -func (r *Router) listDeviceHandler(c *gin.Context) { - var deviceList []interface{} - client, err := gadb.NewClient() - if err == nil { - androidDevices, err := client.DeviceList() - if err == nil { - for _, device := range androidDevices { - brand, err := device.Brand() - if err != nil { - RenderError(c, err) - return - } - model, err := device.Model() - if err != nil { - RenderError(c, err) - return - } - version, err := device.SdkVersion() - if err != nil { - RenderError(c, err) - return - } - deviceInfo := map[string]interface{}{ - "serial": device.Serial(), - "brand": brand, - "model": model, - "version": version, - "platform": "android", - } - deviceList = append(deviceList, deviceInfo) - } - } - } - iosDevices, err := ios.ListDevices() - if err == nil { - for _, dev := range iosDevices.DeviceList { - device, err := uixt.NewIOSDevice( - option.WithUDID(dev.Properties.SerialNumber)) - if err != nil { - continue - } - properties := device.Properties - err = ios.Pair(dev) - if err != nil { - log.Error().Err(err).Msg("failed to pair device") - continue - } - version, err := ios.GetProductVersion(dev) - if err != nil { - continue - } - if version.LessThan(semver.MustParse("17.4.0")) && - version.GreaterThan(ios.IOS17()) { - log.Warn().Msg("not support ios 17.0-17.3") - continue - } - plist, err := ios.GetValuesPlist(dev) - if err != nil { - log.Error().Err(err).Msg("failed to get device info") - continue - } - deviceInfo := map[string]interface{}{ - "udid": properties.SerialNumber, - "platform": "ios", - "brand": "apple", - "model": plist["ProductType"], - "version": plist["ProductVersion"], - } - deviceList = append(deviceList, deviceInfo) - } - } - RenderSuccess(c, deviceList) -} - -func createBrowserHandler(c *gin.Context) { - var createBrowserReq CreateBrowserRequest - if err := c.ShouldBindJSON(&createBrowserReq); err != nil { - RenderErrorValidateRequest(c, err) - return - } - - browserInfo, err := uixt.CreateBrowser(createBrowserReq.Timeout, createBrowserReq.Width, createBrowserReq.Height) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, browserInfo) -} - -func (r *Router) deleteBrowserHandler(c *gin.Context) { - driver, err := r.GetDriver(c) - if err != nil { - RenderError(c, err) - return - } - err = driver.DeleteSession() - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) pushImageHandler(c *gin.Context) { - var pushMediaReq PushMediaRequest - if err := c.ShouldBindJSON(&pushMediaReq); err != nil { - RenderErrorValidateRequest(c, err) - return - } - driver, err := r.GetDriver(c) - if err != nil { - return - } - imagePath, err := uixt.DownloadFileByUrl(pushMediaReq.ImageUrl) - if path.Ext(imagePath) == "" { - err = os.Rename(imagePath, imagePath+".png") - if err != nil { - RenderError(c, err) - return - } - imagePath = imagePath + ".png" - } - if err != nil { - RenderError(c, err) - return - } - defer func() { - _ = os.Remove(imagePath) - }() - err = driver.PushImage(imagePath) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) clearImageHandler(c *gin.Context) { - driver, err := r.GetDriver(c) - if err != nil { - return - } - err = driver.ClearImages() - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) videoHandler(c *gin.Context) { - RenderSuccess(c, "") -} diff --git a/server/key.go b/server/key.go deleted file mode 100644 index 8f1e192b..00000000 --- a/server/key.go +++ /dev/null @@ -1,76 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - - "github.com/httprunner/httprunner/v5/uixt" - "github.com/httprunner/httprunner/v5/uixt/option" -) - -func (r *Router) unlockHandler(c *gin.Context) { - driver, err := r.GetDriver(c) - if err != nil { - return - } - err = driver.Unlock() - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) homeHandler(c *gin.Context) { - driver, err := r.GetDriver(c) - if err != nil { - return - } - err = driver.Home() - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) backspaceHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_Backspace) - if err != nil { - return - } - - count := req.Count - if count == 0 { - count = 20 - } - driver, err := r.GetDriver(c) - if err != nil { - return - } - err = driver.Backspace(count) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) keycodeHandler(c *gin.Context) { - 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(req.Keycode), uixt.KMEmpty) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} diff --git a/server/main.go b/server/main.go index 71258846..bb076a4d 100644 --- a/server/main.go +++ b/server/main.go @@ -1,7 +1,12 @@ package server import ( + "context" "fmt" + "net/http" + "os" + "os/signal" + "syscall" "time" "github.com/httprunner/httprunner/v5/mcphost" @@ -39,58 +44,57 @@ func (r *Router) Init() { r.Engine.GET("/ping", r.pingHandler) r.Engine.GET("/", r.pingHandler) r.Engine.POST("/", r.pingHandler) - r.Engine.GET("/api/v1/devices", r.listDeviceHandler) - r.Engine.POST("/api/v1/browser/create_browser", createBrowserHandler) apiV1PlatformSerial := r.Group("/api/v1").Group("/:platform").Group("/:serial") // tool operations apiV1PlatformSerial.POST("/tool/invoke", r.invokeToolHandler) - // UI operations - apiV1PlatformSerial.POST("/ui/tap", r.tapHandler) - apiV1PlatformSerial.POST("/ui/right_click", r.rightClickHandler) - apiV1PlatformSerial.POST("/ui/double_tap", r.doubleTapHandler) - apiV1PlatformSerial.POST("/ui/drag", r.dragHandler) - apiV1PlatformSerial.POST("/ui/input", r.inputHandler) - apiV1PlatformSerial.POST("/ui/home", r.homeHandler) - apiV1PlatformSerial.POST("/ui/upload", r.uploadHandler) - apiV1PlatformSerial.POST("/ui/hover", r.hoverHandler) - apiV1PlatformSerial.POST("/ui/scroll", r.scrollHandler) - - // Key operations - apiV1PlatformSerial.POST("/key/unlock", r.unlockHandler) - apiV1PlatformSerial.POST("/key/home", r.homeHandler) - apiV1PlatformSerial.POST("/key/backspace", r.backspaceHandler) - apiV1PlatformSerial.POST("/key", r.keycodeHandler) - - // APP operations - apiV1PlatformSerial.GET("/app/foreground", r.foregroundAppHandler) - apiV1PlatformSerial.GET("/app/appInfo", r.appInfoHandler) - apiV1PlatformSerial.POST("/app/clear", r.clearAppHandler) - apiV1PlatformSerial.POST("/app/launch", r.launchAppHandler) - apiV1PlatformSerial.POST("/app/terminal", r.terminalAppHandler) - apiV1PlatformSerial.POST("/app/uninstall", r.uninstallAppHandler) - - // Device operations - apiV1PlatformSerial.GET("/screenshot", r.screenshotHandler) - apiV1PlatformSerial.DELETE("/close_browser", r.deleteBrowserHandler) - apiV1PlatformSerial.GET("/video", r.videoHandler) - apiV1PlatformSerial.POST("/device/push_image", r.pushImageHandler) - apiV1PlatformSerial.POST("/device/clear_image", r.clearImageHandler) - apiV1PlatformSerial.GET("/adb/source", r.adbSourceHandler) - // uixt operations apiV1PlatformSerial.POST("/uixt/action", r.uixtActionHandler) apiV1PlatformSerial.POST("/uixt/actions", r.uixtActionsHandler) } func (r *Router) Run(port int) error { - err := r.Engine.Run(fmt.Sprintf("localhost:%d", port)) - if err != nil { - log.Err(err).Msg("failed to start http server") + // Create HTTP server + server := &http.Server{ + Addr: fmt.Sprintf("localhost:%d", port), + Handler: r.Engine, + } + + // Channel to listen for interrupt signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + // Start server in a goroutine + go func() { + log.Info().Int("port", port).Msg("Starting hrp server") + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Error().Err(err).Msg("HTTP server failed to start") + } + }() + + // Wait for interrupt signal + <-quit + log.Info().Msg("Shutting down hrp server...") + + // Create a context with timeout for graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Shutdown MCP host first if it exists + if r.mcpHost != nil { + log.Info().Msg("Shutting down MCP host...") + r.mcpHost.Shutdown() + } + + // Shutdown HTTP server + if err := server.Shutdown(ctx); err != nil { + log.Error().Err(err).Msg("hrp server forced to shutdown") return err } + + log.Info().Msg("hrp server exited") return nil } diff --git a/server/model.go b/server/model.go index 969ca9d2..23c4e4ff 100644 --- a/server/model.go +++ b/server/model.go @@ -1,40 +1,7 @@ package server -import ( - "github.com/httprunner/httprunner/v5/uixt/option" -) - -type uploadRequest struct { - X float64 `json:"x"` - Y float64 `json:"y"` - FileUrl string `json:"file_url"` - FileFormat string `json:"file_format"` -} - -type PushMediaRequest struct { - ImageUrl string `json:"imageUrl" binding:"required_without=VideoUrl"` - VideoUrl string `json:"videoUrl" binding:"required_without=ImageUrl"` -} - type HttpResponse struct { - Code int `json:"errorCode"` - Message string `json:"errorMsg"` + Code int `json:"code"` + Message string `json:"message"` Result interface{} `json:"result,omitempty"` } - -type ScreenRequest struct { - Options *option.ScreenOptions `json:"options,omitempty"` -} - -type UploadRequest struct { - X float64 `json:"x"` - Y float64 `json:"y"` - FileUrl string `json:"file_url"` - FileFormat string `json:"file_format"` -} - -type CreateBrowserRequest struct { - Timeout int `json:"timeout"` - Width int `json:"width"` - Height int `json:"height"` -} diff --git a/server/source.go b/server/source.go deleted file mode 100644 index aac7e204..00000000 --- a/server/source.go +++ /dev/null @@ -1,64 +0,0 @@ -package server - -import ( - "encoding/base64" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v5/uixt/option" -) - -func (r *Router) screenshotHandler(c *gin.Context) { - driver, err := r.GetDriver(c) - if err != nil { - return - } - - raw, err := driver.ScreenShot() - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, base64.StdEncoding.EncodeToString(raw.Bytes())) -} - -func (r *Router) screenResultHandler(c *gin.Context) { - driver, err := r.GetDriver(c) - if err != nil { - return - } - - var screenReq ScreenRequest - if err := c.ShouldBindJSON(&screenReq); err != nil { - RenderErrorValidateRequest(c, err) - return - } - - var actionOptions []option.ActionOption - if screenReq.Options != nil { - actionOptions = screenReq.Options.GetScreenShotOptions() - } - - screenResult, err := driver.GetScreenResult(actionOptions...) - if err != nil { - log.Err(err).Msg("get screen result failed") - RenderError(c, err) - return - } - RenderSuccess(c, screenResult) -} - -func (r *Router) adbSourceHandler(c *gin.Context) { - dExt, err := r.GetDriver(c) - if err != nil { - return - } - - source, err := dExt.Source() - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, source) -} diff --git a/server/ui.go b/server/ui.go deleted file mode 100644 index 8d9e1e0a..00000000 --- a/server/ui.go +++ /dev/null @@ -1,210 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/httprunner/httprunner/v5/uixt" - "github.com/httprunner/httprunner/v5/uixt/option" -) - -// processUnifiedRequest is a helper function to handle common request processing -func (r *Router) processUnifiedRequest(c *gin.Context, actionType option.ActionName) (*option.ActionOptions, error) { - var req option.ActionOptions - - // Bind JSON request - if err := c.ShouldBindJSON(&req); err != nil { - RenderErrorValidateRequest(c, err) - 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.ActionOptions) { - 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 req.Duration > 0 { - err = driver.Drag(req.X, req.Y, req.X, req.Y, - option.WithDuration(req.Duration)) - } else { - err = driver.TapXY(req.X, req.Y) - } - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) rightClickHandler(c *gin.Context) { - 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(req.X, req.Y) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) uploadHandler(c *gin.Context) { - var uploadRequest uploadRequest - if err := c.ShouldBindJSON(&uploadRequest); err != nil { - RenderErrorValidateRequest(c, err) - return - } - - driver, err := r.GetDriver(c) - if err != nil { - RenderError(c, err) - return - } - err = driver.IDriver.(*uixt.BrowserDriver). - UploadFile(uploadRequest.X, uploadRequest.Y, - uploadRequest.FileUrl, uploadRequest.FileFormat) - if err != nil { - c.Abort() - return - } - RenderSuccess(c, true) -} - -func (r *Router) hoverHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_Hover) - if err != nil { - return - } - - driver, err := r.GetDriver(c) - if err != nil { - RenderError(c, err) - return - } - - err = driver.IDriver.(*uixt.BrowserDriver). - Hover(req.X, req.Y) - - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) scrollHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_Scroll) - if err != nil { - return - } - - driver, err := r.GetDriver(c) - if err != nil { - RenderError(c, err) - return - } - - err = driver.IDriver.(*uixt.BrowserDriver). - Scroll(req.Delta) - - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) doubleTapHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_DoubleTap) - if err != nil { - return - } - - driver, err := r.GetDriver(c) - if err != nil { - return - } - - err = driver.DoubleTap(req.X, req.Y) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) dragHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_Drag) - if err != nil { - return - } - - duration := req.Duration - if duration == 0 { - duration = 1 - } - driver, err := r.GetDriver(c) - if err != nil { - return - } - - err = driver.Drag(req.FromX, req.FromY, req.ToX, req.ToY, - option.WithDuration(duration), - option.WithPressDuration(req.PressDuration)) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} - -func (r *Router) inputHandler(c *gin.Context) { - req, err := r.processUnifiedRequest(c, option.ACTION_Input) - if err != nil { - return - } - - driver, err := r.GetDriver(c) - if err != nil { - return - } - err = driver.Input(req.Text, option.WithFrequency(req.Frequency)) - if err != nil { - RenderError(c, err) - return - } - RenderSuccess(c, true) -} diff --git a/server/ui_test.go b/server/ui_test.go index 6d2430bb..c876d670 100644 --- a/server/ui_test.go +++ b/server/ui_test.go @@ -3,76 +3,13 @@ package server import ( "bytes" "encoding/json" - "fmt" "net/http" "net/http/httptest" "testing" - "github.com/httprunner/httprunner/v5/uixt/option" "github.com/stretchr/testify/assert" ) -func TestTapHandler(t *testing.T) { - router := NewRouter() - - tests := []struct { - name string - path string - req option.ActionOptions - wantStatus int - wantResp HttpResponse - }{ - { - name: "tap abs xy", - path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), - req: option.ActionOptions{ - X: 500.0, - Y: 800.0, - Duration: 0, - }, - wantStatus: http.StatusOK, - wantResp: HttpResponse{ - Code: 0, - Message: "success", - Result: true, - }, - }, - { - name: "tap relative xy", - path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"), - req: option.ActionOptions{ - X: 0.5, - Y: 0.6, - Duration: 0, - }, - wantStatus: http.StatusOK, - wantResp: HttpResponse{ - Code: 0, - Message: "success", - Result: true, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - reqBody, _ := json.Marshal(tt.req) - req := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewBuffer(reqBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - assert.Equal(t, tt.wantStatus, w.Code) - - var got HttpResponse - err := json.Unmarshal(w.Body.Bytes(), &got) - assert.NoError(t, err) - assert.Equal(t, tt.wantResp, got) - }) - } -} - func TestInvokeToolHandler(t *testing.T) { router := NewRouter() router.InitMCPHost("../internal/mcp/testdata/test.mcp.json") diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 4bfd3961..5d35a756 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -470,11 +470,6 @@ func (wd *BrowserDriver) AppTerminate(packageName string) (bool, error) { return true, wd.DeleteSession() } -// AssertForegroundApp returns nil if the given package and activity are in foreground -func (wd *BrowserDriver) AssertForegroundApp(packageName string, activityType ...string) error { - return nil -} - func (wd *BrowserDriver) Back() error { return wd.PressBack() } diff --git a/uixt/cache.go b/uixt/cache.go index 8501f968..6fee28af 100644 --- a/uixt/cache.go +++ b/uixt/cache.go @@ -10,16 +10,184 @@ import ( "github.com/rs/zerolog/log" ) -var driverCache sync.Map // key is serial, value is *CachedXTDriver - -// CachedXTDriver wraps XTDriver with additional cache metadata -type CachedXTDriver struct { - Platform string - Serial string - Driver *XTDriver - RefCount int32 // reference count for resource management +// CacheManager provides a generic cache management interface +type CacheManager[T any] struct { + cache sync.Map + name string // cache name for logging + cleanup func(T) error // cleanup function for cached items } +// NewCacheManager creates a new cache manager +func NewCacheManager[T any](name string, cleanup func(T) error) *CacheManager[T] { + return &CacheManager[T]{ + cache: sync.Map{}, + name: name, + cleanup: cleanup, + } +} + +// CachedItem wraps an item with cache metadata +type CachedItem[T any] struct { + Key string + Item T + RefCount int32 + Metadata map[string]interface{} // additional metadata +} + +// Get retrieves an item from cache +func (cm *CacheManager[T]) Get(key string) (*CachedItem[T], bool) { + if item, ok := cm.cache.Load(key); ok { + if cached, ok := item.(*CachedItem[T]); ok { + cached.RefCount++ + log.Debug(). + Str("cache", cm.name). + Str("key", key). + Int32("refCount", cached.RefCount). + Msg("Retrieved item from cache") + return cached, true + } + } + return nil, false +} + +// Set stores an item in cache +func (cm *CacheManager[T]) Set(key string, item T, metadata map[string]interface{}) *CachedItem[T] { + cached := &CachedItem[T]{ + Key: key, + Item: item, + RefCount: 1, + Metadata: metadata, + } + + cm.cache.Store(key, cached) + log.Debug(). + Str("cache", cm.name). + Str("key", key). + Msg("Stored item in cache") + + return cached +} + +// Release decrements reference count and removes item if count reaches zero +func (cm *CacheManager[T]) Release(key string) error { + if item, ok := cm.cache.Load(key); ok { + if cached, ok := item.(*CachedItem[T]); ok { + cached.RefCount-- + log.Debug(). + Str("cache", cm.name). + Str("key", key). + Int32("refCount", cached.RefCount). + Msg("Released item reference") + + // If no more references, clean up and remove from cache + if cached.RefCount <= 0 { + cm.cache.Delete(key) + + // Clean up item if cleanup function is provided + if cm.cleanup != nil { + if err := cm.cleanup(cached.Item); err != nil { + log.Warn().Err(err). + Str("cache", cm.name). + Str("key", key). + Msg("Failed to cleanup cached item") + return err + } + } + + log.Info(). + Str("cache", cm.name). + Str("key", key). + Msg("Cleaned up item from cache") + } + } + } + return nil +} + +// Clear removes all items from cache +func (cm *CacheManager[T]) Clear() { + cm.cache.Range(func(key, value interface{}) bool { + if keyStr, ok := key.(string); ok { + if cached, ok := value.(*CachedItem[T]); ok { + // Clean up item if cleanup function is provided + if cm.cleanup != nil { + if err := cm.cleanup(cached.Item); err != nil { + log.Warn().Err(err). + Str("cache", cm.name). + Str("key", keyStr). + Msg("Failed to cleanup cached item") + } + } + log.Debug(). + Str("cache", cm.name). + Str("key", keyStr). + Msg("Cleaned up item from cache") + } + cm.cache.Delete(key) + } + return true + }) + log.Info().Str("cache", cm.name).Msg("Cleared all cached items") +} + +// List returns all cached items +func (cm *CacheManager[T]) List() []CachedItem[T] { + var items []CachedItem[T] + cm.cache.Range(func(key, value interface{}) bool { + if cached, ok := value.(*CachedItem[T]); ok { + items = append(items, *cached) + } + return true + }) + return items +} + +// GetOrCreate gets an existing item or creates a new one using the provided factory function +func (cm *CacheManager[T]) GetOrCreate(key string, factory func() (T, map[string]interface{}, error)) (T, error) { + // Check cache first + if cached, ok := cm.Get(key); ok { + return cached.Item, nil + } + + // Create new item + item, metadata, err := factory() + if err != nil { + var zero T + return zero, fmt.Errorf("failed to create item: %w", err) + } + + // Store in cache + cached := cm.Set(key, item, metadata) + return cached.Item, nil +} + +// Size returns the number of items in cache +func (cm *CacheManager[T]) Size() int { + count := 0 + cm.cache.Range(func(key, value interface{}) bool { + count++ + return true + }) + return count +} + +// Use cache manager for XTDriver caching +var driverCacheManager = NewCacheManager("xt-driver", cleanupXTDriver) + +// cleanupXTDriver cleans up XTDriver resources +func cleanupXTDriver(driver *XTDriver) error { + if driver != nil && driver.IDriver != nil { + if err := driver.DeleteSession(); err != nil { + log.Warn().Err(err).Msg("Failed to delete driver session during cleanup") + return err + } + } + return nil +} + +// CachedXTDriver is an alias for CachedItem[*XTDriver] for backward compatibility +type CachedXTDriver = CachedItem[*XTDriver] + // DriverCacheConfig holds configuration for driver creation type DriverCacheConfig struct { Platform string @@ -30,67 +198,48 @@ type DriverCacheConfig struct { // GetOrCreateXTDriver gets an existing driver from cache or creates a new one func GetOrCreateXTDriver(config DriverCacheConfig) (*XTDriver, error) { - // If serial is specified, check cache first - if config.Serial != "" { - cacheKey := config.Serial - if cachedItem, ok := driverCache.Load(cacheKey); ok { - if cached, ok := cachedItem.(*CachedXTDriver); ok { - log.Info().Str("serial", cached.Serial).Msg("Using cached XTDriver") - - // Increment reference count - cached.RefCount++ - return cached.Driver, nil - } - } - } - - // If no serial specified, try to find existing driver + // Handle empty serial case - try to find existing driver first if config.Serial == "" { if driver := findCachedDriver(config.Platform); driver != nil { return driver, nil } } - // Create new driver (will auto-detect serial if empty) - driverExt, err := createXTDriverWithConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create XTDriver: %w", err) - } - - // Get actual serial from the created driver - actualSerial := driverExt.GetDevice().UUID() - - // Check if a driver with this actual serial already exists in cache - if cachedItem, ok := driverCache.Load(actualSerial); ok { - if cached, ok := cachedItem.(*CachedXTDriver); ok { - log.Info().Str("serial", actualSerial).Msg("Found existing cached XTDriver with detected serial") - - // Clean up the newly created driver since we have a cached one - if err := driverExt.DeleteSession(); err != nil { - log.Warn().Err(err).Str("serial", actualSerial).Msg("Failed to delete newly created driver session") - } - - // Increment reference count and return cached driver - cached.RefCount++ - return cached.Driver, nil + // Use shared cache manager's GetOrCreate functionality + return driverCacheManager.GetOrCreate(config.Serial, func() (*XTDriver, map[string]interface{}, error) { + // Create new driver + driverExt, err := createXTDriverWithConfig(config) + if err != nil { + return nil, nil, fmt.Errorf("failed to create XTDriver: %w", err) } - } - // Cache the new driver with actual serial - cached := &CachedXTDriver{ - Platform: config.Platform, - Driver: driverExt, - Serial: actualSerial, - RefCount: 1, - } - driverCache.Store(actualSerial, cached) + // Get actual serial from the created driver + actualSerial := driverExt.GetDevice().UUID() - log.Info(). - Str("platform", config.Platform). - Str("serial", actualSerial). - Msg("Created and cached new XTDriver") + // Check if a driver with actual serial already exists (for empty serial case) + if config.Serial == "" && actualSerial != "" { + if existingCached, ok := driverCacheManager.Get(actualSerial); ok { + // Clean up the newly created driver since we have a cached one + if err := driverExt.DeleteSession(); err != nil { + log.Warn().Err(err).Str("serial", actualSerial).Msg("Failed to delete newly created driver session") + } + return existingCached.Item, existingCached.Metadata, nil + } + } - return driverExt, nil + // Create metadata + metadata := map[string]interface{}{ + "platform": config.Platform, + "serial": actualSerial, + } + + log.Info(). + Str("platform", config.Platform). + Str("serial", actualSerial). + Msg("Created and cached new XTDriver") + + return driverExt, metadata, nil + }) } // createXTDriverWithConfig creates a new XTDriver based on configuration @@ -184,94 +333,41 @@ func createXTDriverWithConfig(config DriverCacheConfig) (*XTDriver, error) { // ReleaseXTDriver decrements reference count and removes from cache when count reaches zero func ReleaseXTDriver(serial string) error { - if cachedItem, ok := driverCache.Load(serial); ok { - if cached, ok := cachedItem.(*CachedXTDriver); ok { - cached.RefCount-- - log.Debug(). - Str("serial", serial). - Int32("refCount", cached.RefCount). - Msg("Released XTDriver reference") - - // If no more references, clean up and remove from cache - if cached.RefCount <= 0 { - driverCache.Delete(serial) - - // Clean up driver resources if driver has underlying IDriver - if cached.Driver != nil && cached.Driver.IDriver != nil { - if err := cached.Driver.DeleteSession(); err != nil { - log.Warn().Err(err).Str("serial", serial).Msg("Failed to delete driver session") - } - } - - log.Info().Str("serial", serial).Msg("Cleaned up XTDriver from cache") - } - } - } - return nil + return driverCacheManager.Release(serial) } // CleanupAllDrivers cleans up all cached drivers func CleanupAllDrivers() { - driverCache.Range(func(key, value interface{}) bool { - if serial, ok := key.(string); ok { - if cached, ok := value.(*CachedXTDriver); ok { - // Clean up driver resources if driver has underlying IDriver - if cached.Driver != nil && cached.Driver.IDriver != nil { - if err := cached.Driver.DeleteSession(); err != nil { - log.Warn().Err(err).Str("serial", serial).Msg("Failed to delete driver session") - } - } - log.Info().Str("serial", serial).Msg("Cleaned up XTDriver from cache") - } - driverCache.Delete(serial) - } - return true - }) + driverCacheManager.Clear() } // ListCachedDrivers returns information about all cached drivers func ListCachedDrivers() []CachedXTDriver { - var drivers []CachedXTDriver - driverCache.Range(func(key, value interface{}) bool { - if cached, ok := value.(*CachedXTDriver); ok { - drivers = append(drivers, *cached) - } - return true - }) - return drivers + return driverCacheManager.List() } // findCachedDriver searches for a cached driver by platform // If platform is empty, returns any available driver func findCachedDriver(platform string) *XTDriver { - var foundDriver *XTDriver - driverCache.Range(func(key, value interface{}) bool { - serial, ok := key.(string) - if !ok { - return true // continue iteration - } + cachedItems := driverCacheManager.List() - cached, ok := value.(*CachedXTDriver) - if !ok { - return true // continue iteration - } + for _, cachedItem := range cachedItems { + cachedPlatform, _ := cachedItem.Metadata["platform"].(string) // If platform is specified, match platform; otherwise use any available driver - if platform == "" || cached.Platform == platform { - foundDriver = cached.Driver - cached.RefCount++ - - if platform != "" { - log.Debug().Str("platform", platform).Str("serial", serial).Msg("Using cached XTDriver by platform") - } else { - log.Debug().Str("serial", serial).Msg("Using any available cached XTDriver") + if platform == "" || cachedPlatform == platform { + // Increment reference count by getting from cache + if refreshedItem, ok := driverCacheManager.Get(cachedItem.Key); ok { + if platform != "" { + log.Debug().Str("platform", platform).Str("serial", cachedItem.Key).Msg("Using cached XTDriver by platform") + } else { + log.Debug().Str("serial", cachedItem.Key).Msg("Using any available cached XTDriver") + } + return refreshedItem.Item } - return false // stop iteration } - - return true // continue iteration - }) - return foundDriver + } + return nil } // setupXTDriver initializes an XTDriver based on the platform and serial. @@ -310,12 +406,14 @@ func RegisterXTDriver(serial string, driver *XTDriver) error { return fmt.Errorf("driver cannot be nil") } - cached := &CachedXTDriver{ - Driver: driver, - Serial: serial, - RefCount: 1, + // Create metadata + metadata := map[string]interface{}{ + "platform": "external", // Mark as externally registered + "serial": serial, } - driverCache.Store(serial, cached) + + // Store in cache using shared cache manager + driverCacheManager.Set(serial, driver, metadata) log.Info(). Str("serial", serial). @@ -343,8 +441,8 @@ func getXTDriverFromCache(driver IDriver) *XTDriver { // Get XTDriver from cache using device UUID as serial cachedDrivers := ListCachedDrivers() for _, cached := range cachedDrivers { - if cached.Serial == deviceUUID { - return cached.Driver + if serial, _ := cached.Metadata["serial"].(string); serial == deviceUUID { + return cached.Item } } diff --git a/uixt/cache_test.go b/uixt/cache_test.go index 157c59e2..21869e69 100644 --- a/uixt/cache_test.go +++ b/uixt/cache_test.go @@ -33,7 +33,7 @@ func TestGetOrCreateXTDriver_EmptySerial_AutoDetect(t *testing.T) { // Verify that a driver was created and cached with actual serial drivers := ListCachedDrivers() assert.Len(t, drivers, 1) - assert.NotEmpty(t, drivers[0].Serial) // Serial should be populated with actual device serial + assert.NotEmpty(t, drivers[0].Key) // Serial should be populated with actual device serial } } @@ -57,7 +57,7 @@ func TestGetOrCreateXTDriver_EmptySerial_DefaultPlatform(t *testing.T) { // Verify that a driver was created and cached with actual serial drivers := ListCachedDrivers() assert.Len(t, drivers, 1) - assert.NotEmpty(t, drivers[0].Serial) // Serial should be populated with actual device serial + assert.NotEmpty(t, drivers[0].Key) // Serial should be populated with actual device serial } } @@ -168,9 +168,9 @@ func TestRegisterXTDriver_Success(t *testing.T) { // Verify driver is cached drivers := ListCachedDrivers() assert.Len(t, drivers, 1) - assert.Equal(t, "external_001", drivers[0].Serial) + assert.Equal(t, "external_001", drivers[0].Key) assert.Equal(t, int32(1), drivers[0].RefCount) - assert.Equal(t, xtDriver, drivers[0].Driver) + assert.Equal(t, xtDriver, drivers[0].Item) } func TestReleaseXTDriver_NonExistentSerial(t *testing.T) { @@ -255,9 +255,9 @@ func TestListCachedDrivers_Multiple(t *testing.T) { // Verify driver information serials := make(map[string]bool) for _, cached := range drivers { - serials[cached.Serial] = true + serials[cached.Key] = true assert.Equal(t, int32(1), cached.RefCount) - assert.NotNil(t, cached.Driver) + assert.NotNil(t, cached.Item) } assert.True(t, serials["list_test_1"]) assert.True(t, serials["list_test_2"]) @@ -402,7 +402,7 @@ func TestIntegrationExample_TraditionalWay(t *testing.T) { // Verify registration drivers := ListCachedDrivers() assert.Len(t, drivers, 1) - assert.Equal(t, "integration_002", drivers[0].Serial) + assert.Equal(t, "integration_002", drivers[0].Key) // Clean up err = ReleaseXTDriver("integration_002") @@ -555,11 +555,9 @@ func TestCacheReferenceCountManagement(t *testing.T) { assert.Len(t, drivers, 1) assert.Equal(t, int32(1), drivers[0].RefCount) - // Simulate multiple references by manually incrementing - if cachedItem, ok := driverCache.Load(serial); ok { - if cached, ok := cachedItem.(*CachedXTDriver); ok { - cached.RefCount++ - } + // Simulate multiple references by getting from cache (which increments ref count) + if cachedItem, ok := driverCacheManager.Get(serial); ok { + assert.NotNil(t, cachedItem.Item) } // Verify ref count increased diff --git a/uixt/mcp_server.go b/uixt/mcp_server.go index dd30ec4b..06dfb9c3 100644 --- a/uixt/mcp_server.go +++ b/uixt/mcp_server.go @@ -117,7 +117,6 @@ func (s *MCPServer4XTDriver) registerTools() { s.registerTool(&ToolClosePopups{}) // PC/Web Tools - s.registerTool(&ToolWebLoginNoneUI{}) s.registerTool(&ToolSecondaryClick{}) s.registerTool(&ToolHoverBySelector{}) s.registerTool(&ToolTapBySelector{}) @@ -157,8 +156,8 @@ type ActionTool interface { ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) } -// buildMCPCallToolRequest is a helper function to build mcp.CallToolRequest -func buildMCPCallToolRequest(toolName option.ActionName, arguments map[string]any) mcp.CallToolRequest { +// BuildMCPCallToolRequest is a helper function to build mcp.CallToolRequest +func BuildMCPCallToolRequest(toolName option.ActionName, arguments map[string]any) mcp.CallToolRequest { return mcp.CallToolRequest{ Params: struct { Name string `json:"name"` @@ -328,7 +327,7 @@ func NewMCPSuccessResponse(message string, actionTool ActionTool) *mcp.CallToolR "message": message, } - // Add all tool-specific fields at the same level + // Add tool-specific fields if provided toolData := convertToolToData(actionTool) for key, value := range toolData { response[key] = value @@ -337,7 +336,7 @@ func NewMCPSuccessResponse(message string, actionTool ActionTool) *mcp.CallToolR return marshalToMCPResult(response) } -// convertToolToData converts tool struct to map[string]any for Data field +// convertToolToData converts tool struct to map for response func convertToolToData(tool interface{}) map[string]any { data := make(map[string]any) @@ -382,7 +381,7 @@ func convertToolToData(tool interface{}) map[string]any { return data } -// NewMCPErrorResponse creates an error response +// NewMCPErrorResponse creates an error MCP response func NewMCPErrorResponse(message string) *mcp.CallToolResult { response := map[string]any{ "success": false, @@ -420,7 +419,7 @@ func GenerateReturnSchema(toolStruct interface{}) map[string]string { for i := 0; i < structType.NumField(); i++ { field := structType.Field(i) - // Skip embedded MCPResponse fields (though they shouldn't exist now) + // Skip embedded MCPResponse fields if field.Type.Name() == "MCPResponse" { continue } diff --git a/uixt/mcp_tools_ai.go b/uixt/mcp_tools_ai.go index 7ef8c5d2..bc9a42c0 100644 --- a/uixt/mcp_tools_ai.go +++ b/uixt/mcp_tools_ai.go @@ -65,7 +65,7 @@ func (t *ToolStartToGoal) ConvertActionToCallToolRequest(action option.MobileAct // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid start to goal params: %v", action.Params) } @@ -125,7 +125,7 @@ func (t *ToolAIAction) ConvertActionToCallToolRequest(action option.MobileAction // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid AI action params: %v", action.Params) } @@ -190,7 +190,7 @@ func (t *ToolAIQuery) ConvertActionToCallToolRequest(action option.MobileAction) // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid AI query params: %v", action.Params) } @@ -236,7 +236,7 @@ func (t *ToolFinished) ConvertActionToCallToolRequest(action option.MobileAction arguments := map[string]any{ "content": reason, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid finished params: %v", action.Params) } diff --git a/uixt/mcp_tools_app.go b/uixt/mcp_tools_app.go index dc20eea0..417d5fac 100644 --- a/uixt/mcp_tools_app.go +++ b/uixt/mcp_tools_app.go @@ -53,7 +53,7 @@ func (t *ToolListPackages) Implement() server.ToolHandlerFunc { } func (t *ToolListPackages) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } // ToolLaunchApp implements the launch_app tool call. @@ -109,7 +109,7 @@ func (t *ToolLaunchApp) ConvertActionToCallToolRequest(action option.MobileActio arguments := map[string]any{ "packageName": packageName, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid app launch params: %v", action.Params) } @@ -174,7 +174,7 @@ func (t *ToolTerminateApp) ConvertActionToCallToolRequest(action option.MobileAc arguments := map[string]any{ "packageName": packageName, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid app terminate params: %v", action.Params) } @@ -228,7 +228,7 @@ func (t *ToolAppInstall) ConvertActionToCallToolRequest(action option.MobileActi arguments := map[string]any{ "appUrl": appUrl, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid app install params: %v", action.Params) } @@ -282,7 +282,7 @@ func (t *ToolAppUninstall) ConvertActionToCallToolRequest(action option.MobileAc arguments := map[string]any{ "packageName": packageName, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid app uninstall params: %v", action.Params) } @@ -336,7 +336,7 @@ func (t *ToolAppClear) ConvertActionToCallToolRequest(action option.MobileAction arguments := map[string]any{ "packageName": packageName, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid app clear params: %v", action.Params) } @@ -385,5 +385,5 @@ func (t *ToolGetForegroundApp) Implement() server.ToolHandlerFunc { } func (t *ToolGetForegroundApp) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } diff --git a/uixt/mcp_tools_button.go b/uixt/mcp_tools_button.go index 4f891538..f49ae309 100644 --- a/uixt/mcp_tools_button.go +++ b/uixt/mcp_tools_button.go @@ -59,7 +59,7 @@ func (t *ToolPressButton) ConvertActionToCallToolRequest(action option.MobileAct arguments := map[string]any{ "button": button, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid press button params: %v", action.Params) } @@ -102,7 +102,7 @@ func (t *ToolHome) Implement() server.ToolHandlerFunc { } func (t *ToolHome) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } // ToolBack implements the back tool call. @@ -143,5 +143,5 @@ func (t *ToolBack) Implement() server.ToolHandlerFunc { } func (t *ToolBack) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } diff --git a/uixt/mcp_tools_device.go b/uixt/mcp_tools_device.go index 2d44cad2..8a2fa84f 100644 --- a/uixt/mcp_tools_device.go +++ b/uixt/mcp_tools_device.go @@ -82,7 +82,7 @@ func (t *ToolListAvailableDevices) Implement() server.ToolHandlerFunc { } func (t *ToolListAvailableDevices) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } // ToolSelectDevice implements the select_device tool call. @@ -122,7 +122,7 @@ func (t *ToolSelectDevice) Implement() server.ToolHandlerFunc { } func (t *ToolSelectDevice) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } // ToolScreenRecord implements the screenrecord tool call. @@ -212,5 +212,5 @@ func (t *ToolScreenRecord) Implement() server.ToolHandlerFunc { } func (t *ToolScreenRecord) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } diff --git a/uixt/mcp_tools_input.go b/uixt/mcp_tools_input.go index 0485ba13..4286e211 100644 --- a/uixt/mcp_tools_input.go +++ b/uixt/mcp_tools_input.go @@ -62,7 +62,7 @@ func (t *ToolInput) ConvertActionToCallToolRequest(action option.MobileAction) ( arguments := map[string]any{ "text": text, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } // ToolSetIme implements the set_ime tool call. @@ -114,7 +114,7 @@ func (t *ToolSetIme) ConvertActionToCallToolRequest(action option.MobileAction) arguments := map[string]any{ "ime": ime, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid set ime params: %v", action.Params) } diff --git a/uixt/mcp_tools_screen.go b/uixt/mcp_tools_screen.go index 2d4f5393..eaf38261 100644 --- a/uixt/mcp_tools_screen.go +++ b/uixt/mcp_tools_screen.go @@ -46,7 +46,7 @@ func (t *ToolScreenShot) Implement() server.ToolHandlerFunc { } func (t *ToolScreenShot) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } // ToolGetScreenSize implements the get_screen_size tool call. @@ -92,7 +92,7 @@ func (t *ToolGetScreenSize) Implement() server.ToolHandlerFunc { } func (t *ToolGetScreenSize) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } // ToolGetSource implements the get_source tool call. @@ -148,7 +148,7 @@ func (t *ToolGetSource) ConvertActionToCallToolRequest(action option.MobileActio arguments := map[string]any{ "packageName": packageName, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid get source params: %v", action.Params) } diff --git a/uixt/mcp_tools_swipe.go b/uixt/mcp_tools_swipe.go index f5354748..8c6108ba 100644 --- a/uixt/mcp_tools_swipe.go +++ b/uixt/mcp_tools_swipe.go @@ -188,7 +188,7 @@ func (t *ToolSwipeDirection) ConvertActionToCallToolRequest(action option.Mobile // Extract all action options extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe params: %v", action.Params) } @@ -290,7 +290,7 @@ func (t *ToolSwipeCoordinate) ConvertActionToCallToolRequest(action option.Mobil // Extract all action options extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe advanced params: %v", action.Params) } @@ -364,7 +364,7 @@ func (t *ToolSwipeToTapApp) ConvertActionToCallToolRequest(action option.MobileA // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap app params: %v", action.Params) } @@ -441,7 +441,7 @@ func (t *ToolSwipeToTapText) ConvertActionToCallToolRequest(action option.Mobile // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap text params: %v", action.Params) } @@ -529,7 +529,7 @@ func (t *ToolSwipeToTapTexts) ConvertActionToCallToolRequest(action option.Mobil // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } // ToolDrag implements the drag tool call. @@ -618,7 +618,7 @@ func (t *ToolDrag) ConvertActionToCallToolRequest(action option.MobileAction) (m // Extract all action options extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid drag parameters: %v", action.Params) } diff --git a/uixt/mcp_tools_touch.go b/uixt/mcp_tools_touch.go index 85fde536..2ff3376e 100644 --- a/uixt/mcp_tools_touch.go +++ b/uixt/mcp_tools_touch.go @@ -86,7 +86,7 @@ func (t *ToolTapXY) ConvertActionToCallToolRequest(action option.MobileAction) ( // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid tap params: %v", action.Params) } @@ -172,7 +172,7 @@ func (t *ToolTapAbsXY) ConvertActionToCallToolRequest(action option.MobileAction // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid tap abs params: %v", action.Params) } @@ -243,7 +243,7 @@ func (t *ToolTapByOCR) ConvertActionToCallToolRequest(action option.MobileAction // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by OCR params: %v", action.Params) } @@ -309,7 +309,7 @@ func (t *ToolTapByCV) ConvertActionToCallToolRequest(action option.MobileAction) // Extract options to arguments extractActionOptionsToArguments(action.GetOptions(), arguments) - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } // ToolDoubleTapXY implements the double_tap_xy tool call. @@ -372,7 +372,7 @@ func (t *ToolDoubleTapXY) ConvertActionToCallToolRequest(action option.MobileAct "x": x, "y": y, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid double tap params: %v", action.Params) } diff --git a/uixt/mcp_tools_utility.go b/uixt/mcp_tools_utility.go index f7877394..32f1c4d3 100644 --- a/uixt/mcp_tools_utility.go +++ b/uixt/mcp_tools_utility.go @@ -92,7 +92,7 @@ func (t *ToolSleep) ConvertActionToCallToolRequest(action option.MobileAction) ( arguments := map[string]any{ "seconds": action.Params, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } // ToolSleepMS implements the sleep_ms tool call. @@ -160,7 +160,7 @@ func (t *ToolSleepMS) ConvertActionToCallToolRequest(action option.MobileAction) arguments := map[string]any{ "milliseconds": milliseconds, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } // ToolSleepRandom implements the sleep_random tool call. @@ -204,7 +204,7 @@ func (t *ToolSleepRandom) ConvertActionToCallToolRequest(action option.MobileAct arguments := map[string]any{ "params": params, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep random params: %v", action.Params) } @@ -247,5 +247,5 @@ func (t *ToolClosePopups) Implement() server.ToolHandlerFunc { } func (t *ToolClosePopups) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } diff --git a/uixt/mcp_tools_web.go b/uixt/mcp_tools_web.go index e5715b9a..dcab952a 100644 --- a/uixt/mcp_tools_web.go +++ b/uixt/mcp_tools_web.go @@ -63,7 +63,7 @@ func (t *ToolWebLoginNoneUI) Implement() server.ToolHandlerFunc { } func (t *ToolWebLoginNoneUI) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) { - return buildMCPCallToolRequest(t.Name(), map[string]any{}), nil + return BuildMCPCallToolRequest(t.Name(), map[string]any{}), nil } // ToolSecondaryClick implements the secondary_click tool call. @@ -125,7 +125,7 @@ func (t *ToolSecondaryClick) ConvertActionToCallToolRequest(action option.Mobile "x": params[0], "y": params[1], } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid secondary click params: %v", action.Params) } @@ -179,7 +179,7 @@ func (t *ToolHoverBySelector) ConvertActionToCallToolRequest(action option.Mobil arguments := map[string]any{ "selector": selector, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid hover by selector params: %v", action.Params) } @@ -233,7 +233,7 @@ func (t *ToolTapBySelector) ConvertActionToCallToolRequest(action option.MobileA arguments := map[string]any{ "selector": selector, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by selector params: %v", action.Params) } @@ -287,7 +287,7 @@ func (t *ToolSecondaryClickBySelector) ConvertActionToCallToolRequest(action opt arguments := map[string]any{ "selector": selector, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } return mcp.CallToolRequest{}, fmt.Errorf("invalid secondary click by selector params: %v", action.Params) } @@ -361,5 +361,5 @@ func (t *ToolWebCloseTab) ConvertActionToCallToolRequest(action option.MobileAct arguments := map[string]any{ "tabIndex": tabIndex, } - return buildMCPCallToolRequest(t.Name(), arguments), nil + return BuildMCPCallToolRequest(t.Name(), arguments), nil } diff --git a/uixt/option/action.go b/uixt/option/action.go index 54107dad..b9f45b0c 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -3,7 +3,6 @@ package option import ( "context" "encoding/json" - "fmt" "math/rand/v2" "reflect" "strings" @@ -563,188 +562,6 @@ func WithOutputSchema(schema interface{}) ActionOption { } } -// HTTP API direct usage methods - -// ValidateForHTTPAPI validates the request for HTTP API usage -func (o *ActionOptions) ValidateForHTTPAPI(actionType ActionName) error { - // Basic validation - Platform and Serial are set from URL, so skip here - // They will be validated by setRequestContextFromURL - - // Action-specific validation using a more efficient approach - return o.validateActionSpecificFields(actionType) -} - -// validateActionSpecificFields performs action-specific field validation -func (o *ActionOptions) validateActionSpecificFields(actionType ActionName) error { - // Define validation rules for each action type using ActionMethod constants - validationRules := map[ActionName]func() error{ - ACTION_Tap: func() error { - return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) - }, - ACTION_TapXY: func() error { - return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) - }, - ACTION_TapAbsXY: func() error { - return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) - }, - ACTION_DoubleTap: func() error { - return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) - }, - ACTION_DoubleTapXY: func() error { - return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) - }, - ACTION_RightClick: func() error { - return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) - }, - ACTION_SecondaryClick: func() error { - return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) - }, - ACTION_Hover: func() error { - return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0) - }, - ACTION_Drag: func() error { - return o.requireFields("fromX, fromY, toX, toY coordinates", - o.FromX != 0 && o.FromY != 0 && o.ToX != 0 && o.ToY != 0) - }, - ACTION_SwipeCoordinate: func() error { - return o.requireFields("fromX, fromY, toX, toY coordinates", - o.FromX != 0 && o.FromY != 0 && o.ToX != 0 && o.ToY != 0) - }, - ACTION_Swipe: func() error { - return o.requireFields("direction", o.Direction != nil && o.Direction != "") - }, - ACTION_SwipeDirection: func() error { - return o.requireFields("direction", o.Direction != nil && o.Direction != "") - }, - ACTION_Input: func() error { - return o.requireFields("text", o.Text != "") - }, - ACTION_Delete: func() error { - // Count is optional, will use default if not provided - return nil - }, - ACTION_Backspace: func() error { - // Count is optional, will use default if not provided - return nil - }, - ACTION_KeyCode: func() error { - return o.requireFields("keycode", o.Keycode != 0) - }, - ACTION_Scroll: func() error { - return o.requireFields("delta", o.Delta != 0) - }, - ACTION_AppInfo: func() error { - return o.requireFields("packageName", o.PackageName != "") - }, - ACTION_AppClear: func() error { - return o.requireFields("packageName", o.PackageName != "") - }, - ACTION_AppLaunch: func() error { - return o.requireFields("packageName", o.PackageName != "") - }, - ACTION_AppTerminate: func() error { - return o.requireFields("packageName", o.PackageName != "") - }, - ACTION_AppUninstall: func() error { - return o.requireFields("packageName", o.PackageName != "") - }, - ACTION_AppInstall: func() error { - return o.requireFields("appUrl", o.AppUrl != "") - }, - ACTION_GetForegroundApp: func() error { - return nil - }, - ACTION_TapByOCR: func() error { - return o.requireFields("text", o.Text != "") - }, - ACTION_SwipeToTapText: func() error { - return o.requireFields("text", o.Text != "") - }, - ACTION_TapByCV: func() error { - return o.requireFields("imagePath", o.ImagePath != "") - }, - ACTION_SwipeToTapApp: func() error { - return o.requireFields("appName", o.AppName != "") - }, - ACTION_SwipeToTapTexts: func() error { - return o.requireFields("texts array", len(o.Texts) > 0) - }, - ACTION_TapBySelector: func() error { - return o.requireFields("selector", o.Selector != "") - }, - ACTION_HoverBySelector: func() error { - return o.requireFields("selector", o.Selector != "") - }, - ACTION_SecondaryClickBySelector: func() error { - return o.requireFields("selector", o.Selector != "") - }, - ACTION_WebCloseTab: func() error { - return o.requireFields("tabIndex", o.TabIndex != 0) - }, - ACTION_WebLoginNoneUI: func() error { - if o.PackageName == "" || o.PhoneNumber == "" || o.Captcha == "" || o.Password == "" { - return fmt.Errorf("packageName, phoneNumber, captcha, and password are required for web_login_none_ui action") - } - return nil - }, - ACTION_SetIme: func() error { - return o.requireFields("ime", o.Ime != "") - }, - ACTION_GetSource: func() error { - return o.requireFields("packageName", o.PackageName != "") - }, - ACTION_SleepMS: func() error { - return o.requireFields("milliseconds", o.Milliseconds != 0) - }, - ACTION_SleepRandom: func() error { - return o.requireFields("params array", len(o.Params) > 0) - }, - ACTION_AIAction: func() error { - return o.requireFields("prompt", o.Prompt != "") - }, - ACTION_StartToGoal: func() error { - return o.requireFields("prompt", o.Prompt != "") - }, - ACTION_Query: func() error { - return o.requireFields("prompt", o.Prompt != "") - }, - ACTION_Finished: func() error { - return o.requireFields("content", o.Content != "") - }, - ACTION_Upload: func() error { - if o.X == 0 || o.Y == 0 || o.FileUrl == "" { - return fmt.Errorf("x, y coordinates and fileUrl are required for upload action") - } - return nil - }, - ACTION_PushMedia: func() error { - if o.ImageUrl == "" && o.VideoUrl == "" { - return fmt.Errorf("either imageUrl or videoUrl is required for push_media action") - } - return nil - }, - ACTION_CreateBrowser: func() error { - return o.requireFields("timeout", o.Timeout != 0) - }, - } - - // Execute validation rule for the action type - if validator, exists := validationRules[actionType]; exists { - return validator() - } - - // No specific validation needed for this action type - return nil -} - -// requireFields is a helper function to generate consistent error messages -func (o *ActionOptions) requireFields(fieldDesc string, condition bool) error { - if !condition { - return fmt.Errorf("%s is required for this action", fieldDesc) - } - return nil -} - // GetMCPOptions generates MCP tool options for specific action types func (o *ActionOptions) GetMCPOptions(actionType ActionName) []mcp.ToolOption { // Define field mappings for different action types diff --git a/uixt/option/ios.go b/uixt/option/ios.go index 01d17300..411bebdf 100644 --- a/uixt/option/ios.go +++ b/uixt/option/ios.go @@ -66,7 +66,7 @@ const ( // It helps you to handle an arbitrary element as accept button in accept alert command. // The selector should be a valid class chain expression, where the search root is the alert element itself. // The default button location algorithm is used if the provided selector is wrong or does not match any element. - // e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`] + // e.g. **/XCUIElementTypeButton[`label CONTAINS[c] 'accept'`] acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]" dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" )