mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 15:37:35 +08:00
Merge branch 'merge-wings' into 'master'
Merge wings See merge request iesqa/httprunner!103
This commit is contained in:
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2506211542
|
v5.0.0-beta-2506222254
|
||||||
|
|||||||
@@ -681,10 +681,10 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCa
|
|||||||
for _, cached := range uixt.ListCachedDrivers() {
|
for _, cached := range uixt.ListCachedDrivers() {
|
||||||
// add WDA/UIA logs to summary
|
// add WDA/UIA logs to summary
|
||||||
logs := map[string]interface{}{
|
logs := map[string]interface{}{
|
||||||
"uuid": cached.Serial,
|
"uuid": cached.Key,
|
||||||
}
|
}
|
||||||
|
|
||||||
client := cached.Driver
|
client := cached.Item
|
||||||
if client.GetDevice().LogEnabled() {
|
if client.GetDevice().LogEnabled() {
|
||||||
log, err1 := client.StopCaptureLog()
|
log, err1 := client.StopCaptureLog()
|
||||||
if err1 != nil {
|
if err1 != nil {
|
||||||
|
|||||||
-132
@@ -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)
|
|
||||||
}
|
|
||||||
+104
-21
@@ -3,6 +3,8 @@ package server
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -12,10 +14,84 @@ import (
|
|||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Router) GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error) {
|
func (r *Router) GetDevice(c *gin.Context) (uixt.IDevice, error) {
|
||||||
var device uixt.IDevice
|
platform := c.Param("platform")
|
||||||
var driver uixt.IDriver
|
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")
|
deviceObj, exists := c.Get("device")
|
||||||
|
var device uixt.IDevice
|
||||||
|
var err error
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
device, err = r.GetDevice(c)
|
device, err = r.GetDevice(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -25,32 +101,24 @@ func (r *Router) GetDriver(c *gin.Context) (driverExt *uixt.XTDriver, err error)
|
|||||||
device = deviceObj.(uixt.IDevice)
|
device = deviceObj.(uixt.IDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
driver, err = device.NewDriver()
|
// Create driver
|
||||||
|
driver, err := device.NewDriver()
|
||||||
if err != nil {
|
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)
|
RenderErrorInitDriver(c, err)
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
driverExt, err = uixt.NewXTDriver(driver,
|
// Create XTDriver wrapper
|
||||||
option.WithCVService(option.CVServiceTypeVEDEM))
|
xtDriver, err := uixt.NewXTDriver(driver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderErrorInitDriver(c, err)
|
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) {
|
c.Set("driver", xtDriver)
|
||||||
platform := c.Param("platform")
|
return xtDriver, nil
|
||||||
serial := c.Param("serial")
|
|
||||||
device, err = uixt.NewDeviceWithDefault(platform, serial)
|
|
||||||
if err != nil {
|
|
||||||
RenderErrorInitDriver(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Set("device", device)
|
|
||||||
return device, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderSuccess(c *gin.Context, result interface{}) {
|
func RenderSuccess(c *gin.Context, result interface{}) {
|
||||||
@@ -87,6 +155,21 @@ func RenderErrorInitDriver(c *gin.Context, err error) {
|
|||||||
c.Abort()
|
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) {
|
func RenderErrorValidateRequest(c *gin.Context, err error) {
|
||||||
log.Error().Err(err).Msg("validate request failed")
|
log.Error().Err(err).Msg("validate request failed")
|
||||||
c.JSON(http.StatusBadRequest, HttpResponse{
|
c.JSON(http.StatusBadRequest, HttpResponse{
|
||||||
|
|||||||
@@ -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, "")
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
+42
-38
@@ -1,7 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/mcphost"
|
"github.com/httprunner/httprunner/v5/mcphost"
|
||||||
@@ -39,58 +44,57 @@ func (r *Router) Init() {
|
|||||||
r.Engine.GET("/ping", r.pingHandler)
|
r.Engine.GET("/ping", r.pingHandler)
|
||||||
r.Engine.GET("/", r.pingHandler)
|
r.Engine.GET("/", r.pingHandler)
|
||||||
r.Engine.POST("/", 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")
|
apiV1PlatformSerial := r.Group("/api/v1").Group("/:platform").Group("/:serial")
|
||||||
|
|
||||||
// tool operations
|
// tool operations
|
||||||
apiV1PlatformSerial.POST("/tool/invoke", r.invokeToolHandler)
|
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
|
// uixt operations
|
||||||
apiV1PlatformSerial.POST("/uixt/action", r.uixtActionHandler)
|
apiV1PlatformSerial.POST("/uixt/action", r.uixtActionHandler)
|
||||||
apiV1PlatformSerial.POST("/uixt/actions", r.uixtActionsHandler)
|
apiV1PlatformSerial.POST("/uixt/actions", r.uixtActionsHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) Run(port int) error {
|
func (r *Router) Run(port int) error {
|
||||||
err := r.Engine.Run(fmt.Sprintf("localhost:%d", port))
|
// Create HTTP server
|
||||||
if err != nil {
|
server := &http.Server{
|
||||||
log.Err(err).Msg("failed to start 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("hrp server exited")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-35
@@ -1,40 +1,7 @@
|
|||||||
package server
|
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 {
|
type HttpResponse struct {
|
||||||
Code int `json:"errorCode"`
|
Code int `json:"code"`
|
||||||
Message string `json:"errorMsg"`
|
Message string `json:"message"`
|
||||||
Result interface{} `json:"result,omitempty"`
|
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"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
-210
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -3,76 +3,13 @@ package server
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestInvokeToolHandler(t *testing.T) {
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
router.InitMCPHost("../internal/mcp/testdata/test.mcp.json")
|
router.InitMCPHost("../internal/mcp/testdata/test.mcp.json")
|
||||||
|
|||||||
@@ -470,11 +470,6 @@ func (wd *BrowserDriver) AppTerminate(packageName string) (bool, error) {
|
|||||||
return true, wd.DeleteSession()
|
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 {
|
func (wd *BrowserDriver) Back() error {
|
||||||
return wd.PressBack()
|
return wd.PressBack()
|
||||||
}
|
}
|
||||||
|
|||||||
+234
-136
@@ -10,16 +10,184 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var driverCache sync.Map // key is serial, value is *CachedXTDriver
|
// CacheManager provides a generic cache management interface
|
||||||
|
type CacheManager[T any] struct {
|
||||||
// CachedXTDriver wraps XTDriver with additional cache metadata
|
cache sync.Map
|
||||||
type CachedXTDriver struct {
|
name string // cache name for logging
|
||||||
Platform string
|
cleanup func(T) error // cleanup function for cached items
|
||||||
Serial string
|
|
||||||
Driver *XTDriver
|
|
||||||
RefCount int32 // reference count for resource management
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// DriverCacheConfig holds configuration for driver creation
|
||||||
type DriverCacheConfig struct {
|
type DriverCacheConfig struct {
|
||||||
Platform string
|
Platform string
|
||||||
@@ -30,67 +198,48 @@ type DriverCacheConfig struct {
|
|||||||
|
|
||||||
// GetOrCreateXTDriver gets an existing driver from cache or creates a new one
|
// GetOrCreateXTDriver gets an existing driver from cache or creates a new one
|
||||||
func GetOrCreateXTDriver(config DriverCacheConfig) (*XTDriver, error) {
|
func GetOrCreateXTDriver(config DriverCacheConfig) (*XTDriver, error) {
|
||||||
// If serial is specified, check cache first
|
// Handle empty serial case - try to find existing driver 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
|
|
||||||
if config.Serial == "" {
|
if config.Serial == "" {
|
||||||
if driver := findCachedDriver(config.Platform); driver != nil {
|
if driver := findCachedDriver(config.Platform); driver != nil {
|
||||||
return driver, nil
|
return driver, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new driver (will auto-detect serial if empty)
|
// Use shared cache manager's GetOrCreate functionality
|
||||||
driverExt, err := createXTDriverWithConfig(config)
|
return driverCacheManager.GetOrCreate(config.Serial, func() (*XTDriver, map[string]interface{}, error) {
|
||||||
if err != nil {
|
// Create new driver
|
||||||
return nil, fmt.Errorf("failed to create XTDriver: %w", err)
|
driverExt, err := createXTDriverWithConfig(config)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, 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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the new driver with actual serial
|
// Get actual serial from the created driver
|
||||||
cached := &CachedXTDriver{
|
actualSerial := driverExt.GetDevice().UUID()
|
||||||
Platform: config.Platform,
|
|
||||||
Driver: driverExt,
|
|
||||||
Serial: actualSerial,
|
|
||||||
RefCount: 1,
|
|
||||||
}
|
|
||||||
driverCache.Store(actualSerial, cached)
|
|
||||||
|
|
||||||
log.Info().
|
// Check if a driver with actual serial already exists (for empty serial case)
|
||||||
Str("platform", config.Platform).
|
if config.Serial == "" && actualSerial != "" {
|
||||||
Str("serial", actualSerial).
|
if existingCached, ok := driverCacheManager.Get(actualSerial); ok {
|
||||||
Msg("Created and cached new XTDriver")
|
// 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
|
// 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
|
// ReleaseXTDriver decrements reference count and removes from cache when count reaches zero
|
||||||
func ReleaseXTDriver(serial string) error {
|
func ReleaseXTDriver(serial string) error {
|
||||||
if cachedItem, ok := driverCache.Load(serial); ok {
|
return driverCacheManager.Release(serial)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanupAllDrivers cleans up all cached drivers
|
// CleanupAllDrivers cleans up all cached drivers
|
||||||
func CleanupAllDrivers() {
|
func CleanupAllDrivers() {
|
||||||
driverCache.Range(func(key, value interface{}) bool {
|
driverCacheManager.Clear()
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCachedDrivers returns information about all cached drivers
|
// ListCachedDrivers returns information about all cached drivers
|
||||||
func ListCachedDrivers() []CachedXTDriver {
|
func ListCachedDrivers() []CachedXTDriver {
|
||||||
var drivers []CachedXTDriver
|
return driverCacheManager.List()
|
||||||
driverCache.Range(func(key, value interface{}) bool {
|
|
||||||
if cached, ok := value.(*CachedXTDriver); ok {
|
|
||||||
drivers = append(drivers, *cached)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return drivers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// findCachedDriver searches for a cached driver by platform
|
// findCachedDriver searches for a cached driver by platform
|
||||||
// If platform is empty, returns any available driver
|
// If platform is empty, returns any available driver
|
||||||
func findCachedDriver(platform string) *XTDriver {
|
func findCachedDriver(platform string) *XTDriver {
|
||||||
var foundDriver *XTDriver
|
cachedItems := driverCacheManager.List()
|
||||||
driverCache.Range(func(key, value interface{}) bool {
|
|
||||||
serial, ok := key.(string)
|
|
||||||
if !ok {
|
|
||||||
return true // continue iteration
|
|
||||||
}
|
|
||||||
|
|
||||||
cached, ok := value.(*CachedXTDriver)
|
for _, cachedItem := range cachedItems {
|
||||||
if !ok {
|
cachedPlatform, _ := cachedItem.Metadata["platform"].(string)
|
||||||
return true // continue iteration
|
|
||||||
}
|
|
||||||
|
|
||||||
// If platform is specified, match platform; otherwise use any available driver
|
// If platform is specified, match platform; otherwise use any available driver
|
||||||
if platform == "" || cached.Platform == platform {
|
if platform == "" || cachedPlatform == platform {
|
||||||
foundDriver = cached.Driver
|
// Increment reference count by getting from cache
|
||||||
cached.RefCount++
|
if refreshedItem, ok := driverCacheManager.Get(cachedItem.Key); ok {
|
||||||
|
if platform != "" {
|
||||||
if platform != "" {
|
log.Debug().Str("platform", platform).Str("serial", cachedItem.Key).Msg("Using cached XTDriver by platform")
|
||||||
log.Debug().Str("platform", platform).Str("serial", serial).Msg("Using cached XTDriver by platform")
|
} else {
|
||||||
} else {
|
log.Debug().Str("serial", cachedItem.Key).Msg("Using any available cached XTDriver")
|
||||||
log.Debug().Str("serial", serial).Msg("Using any available cached XTDriver")
|
}
|
||||||
|
return refreshedItem.Item
|
||||||
}
|
}
|
||||||
return false // stop iteration
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true // continue iteration
|
return nil
|
||||||
})
|
|
||||||
return foundDriver
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupXTDriver initializes an XTDriver based on the platform and serial.
|
// 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")
|
return fmt.Errorf("driver cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
cached := &CachedXTDriver{
|
// Create metadata
|
||||||
Driver: driver,
|
metadata := map[string]interface{}{
|
||||||
Serial: serial,
|
"platform": "external", // Mark as externally registered
|
||||||
RefCount: 1,
|
"serial": serial,
|
||||||
}
|
}
|
||||||
driverCache.Store(serial, cached)
|
|
||||||
|
// Store in cache using shared cache manager
|
||||||
|
driverCacheManager.Set(serial, driver, metadata)
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("serial", serial).
|
Str("serial", serial).
|
||||||
@@ -343,8 +441,8 @@ func getXTDriverFromCache(driver IDriver) *XTDriver {
|
|||||||
// Get XTDriver from cache using device UUID as serial
|
// Get XTDriver from cache using device UUID as serial
|
||||||
cachedDrivers := ListCachedDrivers()
|
cachedDrivers := ListCachedDrivers()
|
||||||
for _, cached := range cachedDrivers {
|
for _, cached := range cachedDrivers {
|
||||||
if cached.Serial == deviceUUID {
|
if serial, _ := cached.Metadata["serial"].(string); serial == deviceUUID {
|
||||||
return cached.Driver
|
return cached.Item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-12
@@ -33,7 +33,7 @@ func TestGetOrCreateXTDriver_EmptySerial_AutoDetect(t *testing.T) {
|
|||||||
// Verify that a driver was created and cached with actual serial
|
// Verify that a driver was created and cached with actual serial
|
||||||
drivers := ListCachedDrivers()
|
drivers := ListCachedDrivers()
|
||||||
assert.Len(t, drivers, 1)
|
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
|
// Verify that a driver was created and cached with actual serial
|
||||||
drivers := ListCachedDrivers()
|
drivers := ListCachedDrivers()
|
||||||
assert.Len(t, drivers, 1)
|
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
|
// Verify driver is cached
|
||||||
drivers := ListCachedDrivers()
|
drivers := ListCachedDrivers()
|
||||||
assert.Len(t, drivers, 1)
|
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, 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) {
|
func TestReleaseXTDriver_NonExistentSerial(t *testing.T) {
|
||||||
@@ -255,9 +255,9 @@ func TestListCachedDrivers_Multiple(t *testing.T) {
|
|||||||
// Verify driver information
|
// Verify driver information
|
||||||
serials := make(map[string]bool)
|
serials := make(map[string]bool)
|
||||||
for _, cached := range drivers {
|
for _, cached := range drivers {
|
||||||
serials[cached.Serial] = true
|
serials[cached.Key] = true
|
||||||
assert.Equal(t, int32(1), cached.RefCount)
|
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_1"])
|
||||||
assert.True(t, serials["list_test_2"])
|
assert.True(t, serials["list_test_2"])
|
||||||
@@ -402,7 +402,7 @@ func TestIntegrationExample_TraditionalWay(t *testing.T) {
|
|||||||
// Verify registration
|
// Verify registration
|
||||||
drivers := ListCachedDrivers()
|
drivers := ListCachedDrivers()
|
||||||
assert.Len(t, drivers, 1)
|
assert.Len(t, drivers, 1)
|
||||||
assert.Equal(t, "integration_002", drivers[0].Serial)
|
assert.Equal(t, "integration_002", drivers[0].Key)
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
err = ReleaseXTDriver("integration_002")
|
err = ReleaseXTDriver("integration_002")
|
||||||
@@ -555,11 +555,9 @@ func TestCacheReferenceCountManagement(t *testing.T) {
|
|||||||
assert.Len(t, drivers, 1)
|
assert.Len(t, drivers, 1)
|
||||||
assert.Equal(t, int32(1), drivers[0].RefCount)
|
assert.Equal(t, int32(1), drivers[0].RefCount)
|
||||||
|
|
||||||
// Simulate multiple references by manually incrementing
|
// Simulate multiple references by getting from cache (which increments ref count)
|
||||||
if cachedItem, ok := driverCache.Load(serial); ok {
|
if cachedItem, ok := driverCacheManager.Get(serial); ok {
|
||||||
if cached, ok := cachedItem.(*CachedXTDriver); ok {
|
assert.NotNil(t, cachedItem.Item)
|
||||||
cached.RefCount++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify ref count increased
|
// Verify ref count increased
|
||||||
|
|||||||
+6
-7
@@ -117,7 +117,6 @@ func (s *MCPServer4XTDriver) registerTools() {
|
|||||||
s.registerTool(&ToolClosePopups{})
|
s.registerTool(&ToolClosePopups{})
|
||||||
|
|
||||||
// PC/Web Tools
|
// PC/Web Tools
|
||||||
s.registerTool(&ToolWebLoginNoneUI{})
|
|
||||||
s.registerTool(&ToolSecondaryClick{})
|
s.registerTool(&ToolSecondaryClick{})
|
||||||
s.registerTool(&ToolHoverBySelector{})
|
s.registerTool(&ToolHoverBySelector{})
|
||||||
s.registerTool(&ToolTapBySelector{})
|
s.registerTool(&ToolTapBySelector{})
|
||||||
@@ -157,8 +156,8 @@ type ActionTool interface {
|
|||||||
ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error)
|
ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildMCPCallToolRequest is a helper function to build mcp.CallToolRequest
|
// BuildMCPCallToolRequest is a helper function to build mcp.CallToolRequest
|
||||||
func buildMCPCallToolRequest(toolName option.ActionName, arguments map[string]any) mcp.CallToolRequest {
|
func BuildMCPCallToolRequest(toolName option.ActionName, arguments map[string]any) mcp.CallToolRequest {
|
||||||
return mcp.CallToolRequest{
|
return mcp.CallToolRequest{
|
||||||
Params: struct {
|
Params: struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -328,7 +327,7 @@ func NewMCPSuccessResponse(message string, actionTool ActionTool) *mcp.CallToolR
|
|||||||
"message": message,
|
"message": message,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all tool-specific fields at the same level
|
// Add tool-specific fields if provided
|
||||||
toolData := convertToolToData(actionTool)
|
toolData := convertToolToData(actionTool)
|
||||||
for key, value := range toolData {
|
for key, value := range toolData {
|
||||||
response[key] = value
|
response[key] = value
|
||||||
@@ -337,7 +336,7 @@ func NewMCPSuccessResponse(message string, actionTool ActionTool) *mcp.CallToolR
|
|||||||
return marshalToMCPResult(response)
|
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 {
|
func convertToolToData(tool interface{}) map[string]any {
|
||||||
data := make(map[string]any)
|
data := make(map[string]any)
|
||||||
|
|
||||||
@@ -382,7 +381,7 @@ func convertToolToData(tool interface{}) map[string]any {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMCPErrorResponse creates an error response
|
// NewMCPErrorResponse creates an error MCP response
|
||||||
func NewMCPErrorResponse(message string) *mcp.CallToolResult {
|
func NewMCPErrorResponse(message string) *mcp.CallToolResult {
|
||||||
response := map[string]any{
|
response := map[string]any{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -420,7 +419,7 @@ func GenerateReturnSchema(toolStruct interface{}) map[string]string {
|
|||||||
for i := 0; i < structType.NumField(); i++ {
|
for i := 0; i < structType.NumField(); i++ {
|
||||||
field := structType.Field(i)
|
field := structType.Field(i)
|
||||||
|
|
||||||
// Skip embedded MCPResponse fields (though they shouldn't exist now)
|
// Skip embedded MCPResponse fields
|
||||||
if field.Type.Name() == "MCPResponse" {
|
if field.Type.Name() == "MCPResponse" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (t *ToolStartToGoal) ConvertActionToCallToolRequest(action option.MobileAct
|
|||||||
// Extract options to arguments
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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)
|
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
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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)
|
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
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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)
|
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{
|
arguments := map[string]any{
|
||||||
"content": reason,
|
"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)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid finished params: %v", action.Params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (t *ToolListPackages) Implement() server.ToolHandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ToolListPackages) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
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.
|
// ToolLaunchApp implements the launch_app tool call.
|
||||||
@@ -109,7 +109,7 @@ func (t *ToolLaunchApp) ConvertActionToCallToolRequest(action option.MobileActio
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"packageName": packageName,
|
"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)
|
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{
|
arguments := map[string]any{
|
||||||
"packageName": packageName,
|
"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)
|
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{
|
arguments := map[string]any{
|
||||||
"appUrl": appUrl,
|
"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)
|
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{
|
arguments := map[string]any{
|
||||||
"packageName": packageName,
|
"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)
|
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{
|
arguments := map[string]any{
|
||||||
"packageName": packageName,
|
"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)
|
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) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (t *ToolPressButton) ConvertActionToCallToolRequest(action option.MobileAct
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"button": button,
|
"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)
|
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) {
|
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.
|
// 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) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (t *ToolListAvailableDevices) Implement() server.ToolHandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ToolListAvailableDevices) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
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.
|
// 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) {
|
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.
|
// 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) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (t *ToolInput) ConvertActionToCallToolRequest(action option.MobileAction) (
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"text": text,
|
"text": text,
|
||||||
}
|
}
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return BuildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolSetIme implements the set_ime tool call.
|
// ToolSetIme implements the set_ime tool call.
|
||||||
@@ -114,7 +114,7 @@ func (t *ToolSetIme) ConvertActionToCallToolRequest(action option.MobileAction)
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"ime": ime,
|
"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)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid set ime params: %v", action.Params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func (t *ToolScreenShot) Implement() server.ToolHandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ToolScreenShot) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
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.
|
// 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) {
|
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.
|
// ToolGetSource implements the get_source tool call.
|
||||||
@@ -148,7 +148,7 @@ func (t *ToolGetSource) ConvertActionToCallToolRequest(action option.MobileActio
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"packageName": packageName,
|
"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)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid get source params: %v", action.Params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ func (t *ToolSwipeDirection) ConvertActionToCallToolRequest(action option.Mobile
|
|||||||
// Extract all action options
|
// Extract all action options
|
||||||
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
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)
|
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
|
// Extract all action options
|
||||||
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
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)
|
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
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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)
|
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
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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)
|
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
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
||||||
|
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return BuildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolDrag implements the drag tool call.
|
// ToolDrag implements the drag tool call.
|
||||||
@@ -618,7 +618,7 @@ func (t *ToolDrag) ConvertActionToCallToolRequest(action option.MobileAction) (m
|
|||||||
// Extract all action options
|
// Extract all action options
|
||||||
extractActionOptionsToArguments(action.GetOptions(), arguments)
|
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)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid drag parameters: %v", action.Params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func (t *ToolTapXY) ConvertActionToCallToolRequest(action option.MobileAction) (
|
|||||||
// Extract options to arguments
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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)
|
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
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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)
|
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
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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)
|
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
|
// Extract options to arguments
|
||||||
extractActionOptionsToArguments(action.GetOptions(), 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.
|
// ToolDoubleTapXY implements the double_tap_xy tool call.
|
||||||
@@ -372,7 +372,7 @@ func (t *ToolDoubleTapXY) ConvertActionToCallToolRequest(action option.MobileAct
|
|||||||
"x": x,
|
"x": x,
|
||||||
"y": y,
|
"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)
|
return mcp.CallToolRequest{}, fmt.Errorf("invalid double tap params: %v", action.Params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func (t *ToolSleep) ConvertActionToCallToolRequest(action option.MobileAction) (
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"seconds": action.Params,
|
"seconds": action.Params,
|
||||||
}
|
}
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return BuildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolSleepMS implements the sleep_ms tool call.
|
// ToolSleepMS implements the sleep_ms tool call.
|
||||||
@@ -160,7 +160,7 @@ func (t *ToolSleepMS) ConvertActionToCallToolRequest(action option.MobileAction)
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"milliseconds": milliseconds,
|
"milliseconds": milliseconds,
|
||||||
}
|
}
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return BuildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolSleepRandom implements the sleep_random tool call.
|
// ToolSleepRandom implements the sleep_random tool call.
|
||||||
@@ -204,7 +204,7 @@ func (t *ToolSleepRandom) ConvertActionToCallToolRequest(action option.MobileAct
|
|||||||
arguments := map[string]any{
|
arguments := map[string]any{
|
||||||
"params": params,
|
"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)
|
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) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (t *ToolWebLoginNoneUI) Implement() server.ToolHandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ToolWebLoginNoneUI) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
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.
|
// ToolSecondaryClick implements the secondary_click tool call.
|
||||||
@@ -125,7 +125,7 @@ func (t *ToolSecondaryClick) ConvertActionToCallToolRequest(action option.Mobile
|
|||||||
"x": params[0],
|
"x": params[0],
|
||||||
"y": params[1],
|
"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)
|
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{
|
arguments := map[string]any{
|
||||||
"selector": selector,
|
"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)
|
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{
|
arguments := map[string]any{
|
||||||
"selector": selector,
|
"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)
|
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{
|
arguments := map[string]any{
|
||||||
"selector": selector,
|
"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)
|
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{
|
arguments := map[string]any{
|
||||||
"tabIndex": tabIndex,
|
"tabIndex": tabIndex,
|
||||||
}
|
}
|
||||||
return buildMCPCallToolRequest(t.Name(), arguments), nil
|
return BuildMCPCallToolRequest(t.Name(), arguments), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package option
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"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
|
// GetMCPOptions generates MCP tool options for specific action types
|
||||||
func (o *ActionOptions) GetMCPOptions(actionType ActionName) []mcp.ToolOption {
|
func (o *ActionOptions) GetMCPOptions(actionType ActionName) []mcp.ToolOption {
|
||||||
// Define field mappings for different action types
|
// Define field mappings for different action types
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ const (
|
|||||||
// It helps you to handle an arbitrary element as accept button in accept alert command.
|
// 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 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.
|
// 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 {'允许','好','仅在使用应用期间','稍后再说'}`]"
|
acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]"
|
||||||
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
|
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user