mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 23:51:25 +08:00
refactor: complete ActionOptions unification and pointer type optimization
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
"android": [
|
"android": [
|
||||||
{
|
{
|
||||||
"serial": "$device",
|
"serial": "$device",
|
||||||
"log_on": true,
|
"log_on": false,
|
||||||
"adb_server_host": "localhost",
|
"adb_server_host": "localhost",
|
||||||
"adb_server_port": 5037,
|
"adb_server_port": 5037,
|
||||||
"uia2_ip": "localhost",
|
"uia2_ip": "localhost",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2505271149
|
v5.0.0-beta-2505271334
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ func (r *Router) foregroundAppHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) appInfoHandler(c *gin.Context) {
|
func (r *Router) appInfoHandler(c *gin.Context) {
|
||||||
var req option.UnifiedActionRequest
|
var req option.ActionOptions
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
RenderErrorValidateRequest(c, err)
|
RenderErrorValidateRequest(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
+2
-2
@@ -39,7 +39,7 @@ func (r *Router) backspaceHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
count := req.GetCount()
|
count := req.Count
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
count = 20
|
count = 20
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ func (r *Router) keycodeHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
// TODO FIXME
|
// TODO FIXME
|
||||||
err = driver.IDriver.(*uixt.ADBDriver).
|
err = driver.IDriver.(*uixt.ADBDriver).
|
||||||
PressKeyCode(uixt.KeyCode(req.GetKeycode()), uixt.KMEmpty)
|
PressKeyCode(uixt.KeyCode(req.Keycode), uixt.KMEmpty)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
+15
-16
@@ -7,8 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// processUnifiedRequest is a helper function to handle common request processing
|
// processUnifiedRequest is a helper function to handle common request processing
|
||||||
func (r *Router) processUnifiedRequest(c *gin.Context, actionType option.ActionMethod) (*option.UnifiedActionRequest, error) {
|
func (r *Router) processUnifiedRequest(c *gin.Context, actionType option.ActionName) (*option.ActionOptions, error) {
|
||||||
var req option.UnifiedActionRequest
|
var req option.ActionOptions
|
||||||
|
|
||||||
// Bind JSON request
|
// Bind JSON request
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -29,7 +29,7 @@ func (r *Router) processUnifiedRequest(c *gin.Context, actionType option.ActionM
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setRequestContextFromURL sets platform and serial from URL parameters
|
// setRequestContextFromURL sets platform and serial from URL parameters
|
||||||
func setRequestContextFromURL(c *gin.Context, req *option.UnifiedActionRequest) {
|
func setRequestContextFromURL(c *gin.Context, req *option.ActionOptions) {
|
||||||
if req.Platform == "" {
|
if req.Platform == "" {
|
||||||
req.Platform = c.Param("platform")
|
req.Platform = c.Param("platform")
|
||||||
}
|
}
|
||||||
@@ -49,12 +49,11 @@ func (r *Router) tapHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use UnifiedActionRequest directly
|
if req.Duration > 0 {
|
||||||
if req.GetDuration() > 0 {
|
err = driver.Drag(req.X, req.Y, req.X, req.Y,
|
||||||
err = driver.Drag(req.GetX(), req.GetY(), req.GetX(), req.GetY(),
|
option.WithDuration(req.Duration))
|
||||||
option.WithDuration(req.GetDuration()))
|
|
||||||
} else {
|
} else {
|
||||||
err = driver.TapXY(req.GetX(), req.GetY())
|
err = driver.TapXY(req.X, req.Y)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
@@ -74,7 +73,7 @@ func (r *Router) rightClickHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = driver.IDriver.(*uixt.BrowserDriver).
|
err = driver.IDriver.(*uixt.BrowserDriver).
|
||||||
SecondaryClick(req.GetX(), req.GetY())
|
SecondaryClick(req.X, req.Y)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
return
|
return
|
||||||
@@ -117,7 +116,7 @@ func (r *Router) hoverHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = driver.IDriver.(*uixt.BrowserDriver).
|
err = driver.IDriver.(*uixt.BrowserDriver).
|
||||||
Hover(req.GetX(), req.GetY())
|
Hover(req.X, req.Y)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
@@ -139,7 +138,7 @@ func (r *Router) scrollHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = driver.IDriver.(*uixt.BrowserDriver).
|
err = driver.IDriver.(*uixt.BrowserDriver).
|
||||||
Scroll(req.GetDelta())
|
Scroll(req.Delta)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
@@ -159,7 +158,7 @@ func (r *Router) doubleTapHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = driver.DoubleTap(req.GetX(), req.GetY())
|
err = driver.DoubleTap(req.X, req.Y)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
return
|
return
|
||||||
@@ -173,7 +172,7 @@ func (r *Router) dragHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := req.GetDuration()
|
duration := req.Duration
|
||||||
if duration == 0 {
|
if duration == 0 {
|
||||||
duration = 1
|
duration = 1
|
||||||
}
|
}
|
||||||
@@ -182,9 +181,9 @@ func (r *Router) dragHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = driver.Drag(req.GetFromX(), req.GetFromY(), req.GetToX(), req.GetToY(),
|
err = driver.Drag(req.FromX, req.FromY, req.ToX, req.ToY,
|
||||||
option.WithDuration(duration),
|
option.WithDuration(duration),
|
||||||
option.WithPressDuration(req.GetPressDuration()))
|
option.WithPressDuration(req.PressDuration))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
return
|
return
|
||||||
@@ -202,7 +201,7 @@ func (r *Router) inputHandler(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = driver.Input(req.Text, option.WithFrequency(req.GetFrequency()))
|
err = driver.Input(req.Text, option.WithFrequency(req.Frequency))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RenderError(c, err)
|
RenderError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
+9
-9
@@ -18,17 +18,17 @@ func TestTapHandler(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
path string
|
path string
|
||||||
req option.UnifiedActionRequest
|
req option.ActionOptions
|
||||||
wantStatus int
|
wantStatus int
|
||||||
wantResp HttpResponse
|
wantResp HttpResponse
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "tap abs xy",
|
name: "tap abs xy",
|
||||||
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
|
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
|
||||||
req: option.UnifiedActionRequest{
|
req: option.ActionOptions{
|
||||||
X: &[]float64{500}[0],
|
X: 500.0,
|
||||||
Y: &[]float64{800}[0],
|
Y: 800.0,
|
||||||
Duration: &[]float64{0}[0],
|
Duration: 0,
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantResp: HttpResponse{
|
wantResp: HttpResponse{
|
||||||
@@ -40,10 +40,10 @@ func TestTapHandler(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "tap relative xy",
|
name: "tap relative xy",
|
||||||
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
|
path: fmt.Sprintf("/api/v1/android/%s/ui/tap", "4622ca24"),
|
||||||
req: option.UnifiedActionRequest{
|
req: option.ActionOptions{
|
||||||
X: &[]float64{0.5}[0],
|
X: 0.5,
|
||||||
Y: &[]float64{0.6}[0],
|
Y: 0.6,
|
||||||
Duration: &[]float64{0}[0],
|
Duration: 0,
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantResp: HttpResponse{
|
wantResp: HttpResponse{
|
||||||
|
|||||||
+2
-2
@@ -67,7 +67,7 @@ func (s *StepMobile) Serial(serial string) *StepMobile {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepMobile) Log(actionName option.ActionMethod) *StepMobile {
|
func (s *StepMobile) Log(actionName option.ActionName) *StepMobile {
|
||||||
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
s.obj().Actions = append(s.obj().Actions, uixt.MobileAction{
|
||||||
Method: option.ACTION_LOG,
|
Method: option.ACTION_LOG,
|
||||||
Params: actionName,
|
Params: actionName,
|
||||||
@@ -798,7 +798,7 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
|
|||||||
// stat uixt action
|
// stat uixt action
|
||||||
if action.Method == option.ACTION_LOG {
|
if action.Method == option.ACTION_LOG {
|
||||||
log.Info().Interface("action", action.Params).Msg("stat uixt action")
|
log.Info().Interface("action", action.Params).Msg("stat uixt action")
|
||||||
actionMethod := option.ActionMethod(action.Params.(string))
|
actionMethod := option.ActionName(action.Params.(string))
|
||||||
s.summary.Stat.Actions[actionMethod]++
|
s.summary.Stat.Actions[actionMethod]++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -28,7 +28,7 @@ func NewSummary() *Summary {
|
|||||||
Success: true,
|
Success: true,
|
||||||
Stat: &Stat{
|
Stat: &Stat{
|
||||||
TestSteps: TestStepStat{
|
TestSteps: TestStepStat{
|
||||||
Actions: make(map[option.ActionMethod]int),
|
Actions: make(map[option.ActionName]int),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Time: &TestCaseTime{
|
Time: &TestCaseTime{
|
||||||
@@ -149,7 +149,7 @@ type TestStepStat struct {
|
|||||||
Total int `json:"total" yaml:"total"`
|
Total int `json:"total" yaml:"total"`
|
||||||
Successes int `json:"successes" yaml:"successes"`
|
Successes int `json:"successes" yaml:"successes"`
|
||||||
Failures int `json:"failures" yaml:"failures"`
|
Failures int `json:"failures" yaml:"failures"`
|
||||||
Actions map[option.ActionMethod]int `json:"actions" yaml:"actions"` // record action stats
|
Actions map[option.ActionName]int `json:"actions" yaml:"actions"` // record action stats
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestCaseTime struct {
|
type TestCaseTime struct {
|
||||||
@@ -167,7 +167,7 @@ func NewCaseSummary() *TestCaseSummary {
|
|||||||
return &TestCaseSummary{
|
return &TestCaseSummary{
|
||||||
Success: true,
|
Success: true,
|
||||||
Stat: &TestStepStat{
|
Stat: &TestStepStat{
|
||||||
Actions: make(map[option.ActionMethod]int),
|
Actions: make(map[option.ActionName]int),
|
||||||
},
|
},
|
||||||
Time: &TestCaseTime{
|
Time: &TestCaseTime{
|
||||||
StartAt: time.Now(),
|
StartAt: time.Now(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MobileAction struct {
|
type MobileAction struct {
|
||||||
Method option.ActionMethod `json:"method,omitempty" yaml:"method,omitempty"`
|
Method option.ActionName `json:"method,omitempty" yaml:"method,omitempty"`
|
||||||
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
|
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
|
||||||
Options *option.ActionOptions `json:"options,omitempty" yaml:"options,omitempty"`
|
Options *option.ActionOptions `json:"options,omitempty" yaml:"options,omitempty"`
|
||||||
option.ActionOptions
|
option.ActionOptions
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MarkUIOperation add operation mark for UI operation
|
// MarkUIOperation add operation mark for UI operation
|
||||||
func MarkUIOperation(driver IDriver, actionType option.ActionMethod, actionCoordinates []float64) error {
|
func MarkUIOperation(driver IDriver, actionType option.ActionName, actionCoordinates []float64) error {
|
||||||
if actionType == "" || len(actionCoordinates) == 0 {
|
if actionType == "" || len(actionCoordinates) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, raw
|
|||||||
return fromX, fromY, toX, toY, nil
|
return fromX, fromY, toX, toY, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func preHandler_Swipe(driver IDriver, actionType option.ActionMethod,
|
func preHandler_Swipe(driver IDriver, actionType option.ActionName,
|
||||||
options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) (
|
options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) (
|
||||||
fromX, fromY, toX, toY float64, err error) {
|
fromX, fromY, toX, toY float64, err error) {
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ func preHandler_Swipe(driver IDriver, actionType option.ActionMethod,
|
|||||||
return fromX, fromY, toX, toY, nil
|
return fromX, fromY, toX, toY, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func postHandler(driver IDriver, actionType option.ActionMethod, options *option.ActionOptions) error {
|
func postHandler(driver IDriver, actionType option.ActionName, options *option.ActionOptions) error {
|
||||||
// save screenshot after action
|
// save screenshot after action
|
||||||
if options.PostMarkOperation {
|
if options.PostMarkOperation {
|
||||||
// get compressed screenshot buffer
|
// get compressed screenshot buffer
|
||||||
|
|||||||
+179
-183
File diff suppressed because it is too large
Load Diff
+441
-76
@@ -2,82 +2,87 @@ package option
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActionMethod string
|
type ActionName string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ACTION_LOG ActionMethod = "log"
|
ACTION_LOG ActionName = "log"
|
||||||
ACTION_ListPackages ActionMethod = "list_packages"
|
ACTION_ListPackages ActionName = "list_packages"
|
||||||
ACTION_AppInstall ActionMethod = "app_install"
|
ACTION_AppInstall ActionName = "app_install"
|
||||||
ACTION_AppUninstall ActionMethod = "app_uninstall"
|
ACTION_AppUninstall ActionName = "app_uninstall"
|
||||||
ACTION_WebLoginNoneUI ActionMethod = "web_login_none_ui"
|
ACTION_WebLoginNoneUI ActionName = "web_login_none_ui"
|
||||||
ACTION_AppClear ActionMethod = "app_clear"
|
ACTION_AppClear ActionName = "app_clear"
|
||||||
ACTION_AppStart ActionMethod = "app_start"
|
ACTION_AppStart ActionName = "app_start"
|
||||||
ACTION_AppLaunch ActionMethod = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成
|
ACTION_AppLaunch ActionName = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成
|
||||||
ACTION_AppTerminate ActionMethod = "app_terminate"
|
ACTION_AppTerminate ActionName = "app_terminate"
|
||||||
ACTION_AppStop ActionMethod = "app_stop"
|
ACTION_AppStop ActionName = "app_stop"
|
||||||
ACTION_ScreenShot ActionMethod = "screenshot"
|
ACTION_ScreenShot ActionName = "screenshot"
|
||||||
ACTION_GetScreenSize ActionMethod = "get_screen_size"
|
ACTION_GetScreenSize ActionName = "get_screen_size"
|
||||||
ACTION_Sleep ActionMethod = "sleep"
|
ACTION_Sleep ActionName = "sleep"
|
||||||
ACTION_SleepMS ActionMethod = "sleep_ms"
|
ACTION_SleepMS ActionName = "sleep_ms"
|
||||||
ACTION_SleepRandom ActionMethod = "sleep_random"
|
ACTION_SleepRandom ActionName = "sleep_random"
|
||||||
ACTION_SetIme ActionMethod = "set_ime"
|
ACTION_SetIme ActionName = "set_ime"
|
||||||
ACTION_GetSource ActionMethod = "get_source"
|
ACTION_GetSource ActionName = "get_source"
|
||||||
ACTION_GetForegroundApp ActionMethod = "get_foreground_app"
|
ACTION_GetForegroundApp ActionName = "get_foreground_app"
|
||||||
|
|
||||||
// UI handling
|
// UI handling
|
||||||
ACTION_Home ActionMethod = "home"
|
ACTION_Home ActionName = "home"
|
||||||
ACTION_Tap ActionMethod = "tap" // generic tap action
|
ACTION_Tap ActionName = "tap" // generic tap action
|
||||||
ACTION_TapXY ActionMethod = "tap_xy"
|
ACTION_TapXY ActionName = "tap_xy"
|
||||||
ACTION_TapAbsXY ActionMethod = "tap_abs_xy"
|
ACTION_TapAbsXY ActionName = "tap_abs_xy"
|
||||||
ACTION_TapByOCR ActionMethod = "tap_ocr"
|
ACTION_TapByOCR ActionName = "tap_ocr"
|
||||||
ACTION_TapByCV ActionMethod = "tap_cv"
|
ACTION_TapByCV ActionName = "tap_cv"
|
||||||
ACTION_DoubleTap ActionMethod = "double_tap" // generic double tap action
|
ACTION_DoubleTap ActionName = "double_tap" // generic double tap action
|
||||||
ACTION_DoubleTapXY ActionMethod = "double_tap_xy"
|
ACTION_DoubleTapXY ActionName = "double_tap_xy"
|
||||||
ACTION_Swipe ActionMethod = "swipe" // swipe by direction or coordinates
|
ACTION_Swipe ActionName = "swipe" // swipe by direction or coordinates
|
||||||
ACTION_SwipeDirection ActionMethod = "swipe_direction" // swipe by direction (up, down, left, right)
|
ACTION_SwipeDirection ActionName = "swipe_direction" // swipe by direction (up, down, left, right)
|
||||||
ACTION_SwipeCoordinate ActionMethod = "swipe_coordinate" // swipe by coordinates (fromX, fromY, toX, toY)
|
ACTION_SwipeCoordinate ActionName = "swipe_coordinate" // swipe by coordinates (fromX, fromY, toX, toY)
|
||||||
ACTION_Drag ActionMethod = "drag"
|
ACTION_Drag ActionName = "drag"
|
||||||
ACTION_Input ActionMethod = "input"
|
ACTION_Input ActionName = "input"
|
||||||
ACTION_PressButton ActionMethod = "press_button"
|
ACTION_PressButton ActionName = "press_button"
|
||||||
ACTION_Back ActionMethod = "back"
|
ACTION_Back ActionName = "back"
|
||||||
ACTION_KeyCode ActionMethod = "keycode"
|
ACTION_KeyCode ActionName = "keycode"
|
||||||
ACTION_Delete ActionMethod = "delete" // delete action
|
ACTION_Delete ActionName = "delete" // delete action
|
||||||
ACTION_Backspace ActionMethod = "backspace" // backspace action
|
ACTION_Backspace ActionName = "backspace" // backspace action
|
||||||
ACTION_AIAction ActionMethod = "ai_action" // action with ai
|
ACTION_AIAction ActionName = "ai_action" // action with ai
|
||||||
ACTION_TapBySelector ActionMethod = "tap_by_selector"
|
ACTION_TapBySelector ActionName = "tap_by_selector"
|
||||||
ACTION_HoverBySelector ActionMethod = "hover_by_selector"
|
ACTION_HoverBySelector ActionName = "hover_by_selector"
|
||||||
ACTION_Hover ActionMethod = "hover" // generic hover action
|
ACTION_Hover ActionName = "hover" // generic hover action
|
||||||
ACTION_RightClick ActionMethod = "right_click" // right click action
|
ACTION_RightClick ActionName = "right_click" // right click action
|
||||||
ACTION_WebCloseTab ActionMethod = "web_close_tab"
|
ACTION_WebCloseTab ActionName = "web_close_tab"
|
||||||
ACTION_SecondaryClick ActionMethod = "secondary_click"
|
ACTION_SecondaryClick ActionName = "secondary_click"
|
||||||
ACTION_SecondaryClickBySelector ActionMethod = "secondary_click_by_selector"
|
ACTION_SecondaryClickBySelector ActionName = "secondary_click_by_selector"
|
||||||
ACTION_GetElementTextBySelector ActionMethod = "get_element_text_by_selector"
|
ACTION_GetElementTextBySelector ActionName = "get_element_text_by_selector"
|
||||||
ACTION_Scroll ActionMethod = "scroll" // scroll action
|
ACTION_Scroll ActionName = "scroll" // scroll action
|
||||||
ACTION_Upload ActionMethod = "upload" // upload action
|
ACTION_Upload ActionName = "upload" // upload action
|
||||||
ACTION_PushMedia ActionMethod = "push_media" // push media action
|
ACTION_PushMedia ActionName = "push_media" // push media action
|
||||||
ACTION_CreateBrowser ActionMethod = "create_browser" // create browser action
|
ACTION_CreateBrowser ActionName = "create_browser" // create browser action
|
||||||
ACTION_AppInfo ActionMethod = "app_info" // get app info action
|
ACTION_AppInfo ActionName = "app_info" // get app info action
|
||||||
|
|
||||||
// device actions
|
// device actions
|
||||||
ACTION_ListAvailableDevices ActionMethod = "list_available_devices"
|
ACTION_ListAvailableDevices ActionName = "list_available_devices"
|
||||||
ACTION_SelectDevice ActionMethod = "select_device"
|
ACTION_SelectDevice ActionName = "select_device"
|
||||||
|
|
||||||
// custom actions
|
// custom actions
|
||||||
ACTION_SwipeToTapApp ActionMethod = "swipe_to_tap_app" // swipe left & right to find app and tap
|
ACTION_SwipeToTapApp ActionName = "swipe_to_tap_app" // swipe left & right to find app and tap
|
||||||
ACTION_SwipeToTapText ActionMethod = "swipe_to_tap_text" // swipe up & down to find text and tap
|
ACTION_SwipeToTapText ActionName = "swipe_to_tap_text" // swipe up & down to find text and tap
|
||||||
ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap
|
ACTION_SwipeToTapTexts ActionName = "swipe_to_tap_texts" // swipe up & down to find text and tap
|
||||||
ACTION_ClosePopups ActionMethod = "close_popups"
|
ACTION_ClosePopups ActionName = "close_popups"
|
||||||
ACTION_EndToEndDelay ActionMethod = "live_e2e"
|
ACTION_EndToEndDelay ActionName = "live_e2e"
|
||||||
ACTION_InstallApp ActionMethod = "install_app"
|
ACTION_InstallApp ActionName = "install_app"
|
||||||
ACTION_UninstallApp ActionMethod = "uninstall_app"
|
ACTION_UninstallApp ActionName = "uninstall_app"
|
||||||
ACTION_DownloadApp ActionMethod = "download_app"
|
ACTION_DownloadApp ActionName = "download_app"
|
||||||
ACTION_Finished ActionMethod = "finished"
|
ACTION_Finished ActionName = "finished"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -99,24 +104,79 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ActionOptions struct {
|
type ActionOptions struct {
|
||||||
Context context.Context `json:"-" yaml:"-"`
|
// Device targeting
|
||||||
// log
|
Platform string `json:"platform,omitempty" yaml:"platform,omitempty" binding:"omitempty" desc:"Device platform: android/ios/browser"`
|
||||||
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
|
Serial string `json:"serial,omitempty" yaml:"serial,omitempty" binding:"omitempty" desc:"Device serial/udid/browser id"`
|
||||||
|
|
||||||
// control related
|
// Common action parameters
|
||||||
MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times
|
X float64 `json:"x,omitempty" yaml:"x,omitempty" binding:"omitempty,min=0" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||||
Interval float64 `json:"interval,omitempty" yaml:"interval,omitempty"` // interval between retries in seconds
|
Y float64 `json:"y,omitempty" yaml:"y,omitempty" binding:"omitempty,min=0" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||||
Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` // used to set duration in seconds
|
FromX float64 `json:"from_x,omitempty" yaml:"from_x,omitempty" binding:"omitempty,min=0" desc:"Starting X coordinate"`
|
||||||
PressDuration float64 `json:"press_duration,omitempty" yaml:"press_duration,omitempty"` // used to set press duration in seconds
|
FromY float64 `json:"from_y,omitempty" yaml:"from_y,omitempty" binding:"omitempty,min=0" desc:"Starting Y coordinate"`
|
||||||
Steps int `json:"steps,omitempty" yaml:"steps,omitempty"` // used to set steps of action
|
ToX float64 `json:"to_x,omitempty" yaml:"to_x,omitempty" binding:"omitempty,min=0" desc:"Ending X coordinate"`
|
||||||
Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app
|
ToY float64 `json:"to_y,omitempty" yaml:"to_y,omitempty" binding:"omitempty,min=0" desc:"Ending Y coordinate"`
|
||||||
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action
|
Text string `json:"text,omitempty" yaml:"text,omitempty" desc:"Text content for input/search operations"`
|
||||||
Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty"`
|
|
||||||
|
// App/Package related
|
||||||
|
PackageName string `json:"packageName,omitempty" yaml:"packageName,omitempty" desc:"Package name of the app"`
|
||||||
|
AppName string `json:"appName,omitempty" yaml:"appName,omitempty" desc:"App name to find"`
|
||||||
|
AppUrl string `json:"appUrl,omitempty" yaml:"appUrl,omitempty" desc:"App URL for installation"`
|
||||||
|
MappingUrl string `json:"mappingUrl,omitempty" yaml:"mappingUrl,omitempty" desc:"Mapping URL for app installation"`
|
||||||
|
ResourceMappingUrl string `json:"resourceMappingUrl,omitempty" yaml:"resourceMappingUrl,omitempty" desc:"Resource mapping URL for app installation"`
|
||||||
|
|
||||||
|
// Web/Browser related
|
||||||
|
Selector string `json:"selector,omitempty" yaml:"selector,omitempty" desc:"CSS or XPath selector"`
|
||||||
|
TabIndex int `json:"tabIndex,omitempty" yaml:"tabIndex,omitempty" desc:"Browser tab index"`
|
||||||
|
PhoneNumber string `json:"phoneNumber,omitempty" yaml:"phoneNumber,omitempty" desc:"Phone number for login"`
|
||||||
|
Captcha string `json:"captcha,omitempty" yaml:"captcha,omitempty" desc:"Captcha code"`
|
||||||
|
Password string `json:"password,omitempty" yaml:"password,omitempty" desc:"Password for login"`
|
||||||
|
|
||||||
|
// Button/Key related
|
||||||
|
Button types.DeviceButton `json:"button,omitempty" yaml:"button,omitempty" desc:"Device button to press"`
|
||||||
|
Ime string `json:"ime,omitempty" yaml:"ime,omitempty" desc:"IME package name"`
|
||||||
|
Count int `json:"count,omitempty" yaml:"count,omitempty" desc:"Count for delete operations"`
|
||||||
|
Keycode int `json:"keycode,omitempty" yaml:"keycode,omitempty" desc:"Keycode for key press operations"`
|
||||||
|
|
||||||
|
// Image/CV related
|
||||||
|
ImagePath string `json:"imagePath,omitempty" yaml:"imagePath,omitempty" desc:"Path to reference image for CV recognition"`
|
||||||
|
|
||||||
|
// HTTP API specific fields
|
||||||
|
FileUrl string `json:"file_url,omitempty" yaml:"file_url,omitempty" desc:"File URL for upload operations"`
|
||||||
|
FileFormat string `json:"file_format,omitempty" yaml:"file_format,omitempty" desc:"File format for upload operations"`
|
||||||
|
ImageUrl string `json:"imageUrl,omitempty" yaml:"imageUrl,omitempty" desc:"Image URL for media operations"`
|
||||||
|
VideoUrl string `json:"videoUrl,omitempty" yaml:"videoUrl,omitempty" desc:"Video URL for media operations"`
|
||||||
|
Delta int `json:"delta,omitempty" yaml:"delta,omitempty" desc:"Delta value for scroll operations"`
|
||||||
|
Width int `json:"width,omitempty" yaml:"width,omitempty" desc:"Width for browser creation"`
|
||||||
|
Height int `json:"height,omitempty" yaml:"height,omitempty" desc:"Height for browser creation"`
|
||||||
|
|
||||||
|
// Array parameters
|
||||||
|
Texts []string `json:"texts,omitempty" yaml:"texts,omitempty" desc:"List of texts to search"`
|
||||||
|
Params []float64 `json:"params,omitempty" yaml:"params,omitempty" desc:"Generic parameter array"`
|
||||||
|
|
||||||
|
// AI related
|
||||||
|
Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty" desc:"AI action prompt"`
|
||||||
|
Content string `json:"content,omitempty" yaml:"content,omitempty" desc:"Content for finished action"`
|
||||||
|
|
||||||
|
// Time related
|
||||||
|
Seconds float64 `json:"seconds,omitempty" yaml:"seconds,omitempty" desc:"Sleep duration in seconds"`
|
||||||
|
Milliseconds int64 `json:"milliseconds,omitempty" yaml:"milliseconds,omitempty" desc:"Sleep duration in milliseconds"`
|
||||||
|
|
||||||
|
// Control options
|
||||||
|
Context context.Context `json:"-" yaml:"-"`
|
||||||
|
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty" desc:"Action identifier for logging"`
|
||||||
|
MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty" desc:"Maximum retry times"`
|
||||||
|
Interval float64 `json:"interval,omitempty" yaml:"interval,omitempty" desc:"Interval between retries in seconds"`
|
||||||
|
Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty" desc:"Action duration in seconds"`
|
||||||
|
PressDuration float64 `json:"press_duration,omitempty" yaml:"press_duration,omitempty" desc:"Press duration in seconds"`
|
||||||
|
Steps int `json:"steps,omitempty" yaml:"steps,omitempty" desc:"Number of steps for action"`
|
||||||
|
Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty" desc:"Direction for swipe operations or custom coordinates"`
|
||||||
|
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty" desc:"Timeout in seconds"`
|
||||||
|
Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty" desc:"Action frequency"`
|
||||||
|
|
||||||
ScreenOptions
|
ScreenOptions
|
||||||
|
|
||||||
// set custiom options such as textview, id, description
|
// Custom options
|
||||||
Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"`
|
Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty" desc:"Custom options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ActionOptions) Options() []ActionOption {
|
func (o *ActionOptions) Options() []ActionOption {
|
||||||
@@ -433,3 +493,308 @@ func WithIgnoreNotFoundError(ignoreError bool) ActionOption {
|
|||||||
o.IgnoreNotFoundError = ignoreError
|
o.IgnoreNotFoundError = ignoreError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP API direct usage methods
|
||||||
|
|
||||||
|
// ValidateForHTTPAPI validates the request for HTTP API usage
|
||||||
|
func (o *ActionOptions) ValidateForHTTPAPI(actionType ActionName) error {
|
||||||
|
// Basic validation - Platform and Serial are set from URL, so skip here
|
||||||
|
// They will be validated by setRequestContextFromURL
|
||||||
|
|
||||||
|
// Action-specific validation using a more efficient approach
|
||||||
|
return o.validateActionSpecificFields(actionType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateActionSpecificFields performs action-specific field validation
|
||||||
|
func (o *ActionOptions) validateActionSpecificFields(actionType ActionName) error {
|
||||||
|
// Define validation rules for each action type using ActionMethod constants
|
||||||
|
validationRules := map[ActionName]func() error{
|
||||||
|
ACTION_Tap: func() error {
|
||||||
|
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
|
||||||
|
},
|
||||||
|
ACTION_TapXY: func() error {
|
||||||
|
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
|
||||||
|
},
|
||||||
|
ACTION_TapAbsXY: func() error {
|
||||||
|
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
|
||||||
|
},
|
||||||
|
ACTION_DoubleTap: func() error {
|
||||||
|
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
|
||||||
|
},
|
||||||
|
ACTION_DoubleTapXY: func() error {
|
||||||
|
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
|
||||||
|
},
|
||||||
|
ACTION_RightClick: func() error {
|
||||||
|
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
|
||||||
|
},
|
||||||
|
ACTION_SecondaryClick: func() error {
|
||||||
|
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
|
||||||
|
},
|
||||||
|
ACTION_Hover: func() error {
|
||||||
|
return o.requireFields("x and y coordinates", o.X != 0 && o.Y != 0)
|
||||||
|
},
|
||||||
|
ACTION_Drag: func() error {
|
||||||
|
return o.requireFields("fromX, fromY, toX, toY coordinates",
|
||||||
|
o.FromX != 0 && o.FromY != 0 && o.ToX != 0 && o.ToY != 0)
|
||||||
|
},
|
||||||
|
ACTION_SwipeCoordinate: func() error {
|
||||||
|
return o.requireFields("fromX, fromY, toX, toY coordinates",
|
||||||
|
o.FromX != 0 && o.FromY != 0 && o.ToX != 0 && o.ToY != 0)
|
||||||
|
},
|
||||||
|
ACTION_Swipe: func() error {
|
||||||
|
return o.requireFields("direction", o.Direction != nil && o.Direction != "")
|
||||||
|
},
|
||||||
|
ACTION_SwipeDirection: func() error {
|
||||||
|
return o.requireFields("direction", o.Direction != nil && o.Direction != "")
|
||||||
|
},
|
||||||
|
ACTION_Input: func() error {
|
||||||
|
return o.requireFields("text", o.Text != "")
|
||||||
|
},
|
||||||
|
ACTION_Delete: func() error {
|
||||||
|
// Count is optional, will use default if not provided
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ACTION_Backspace: func() error {
|
||||||
|
// Count is optional, will use default if not provided
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ACTION_KeyCode: func() error {
|
||||||
|
return o.requireFields("keycode", o.Keycode != 0)
|
||||||
|
},
|
||||||
|
ACTION_Scroll: func() error {
|
||||||
|
return o.requireFields("delta", o.Delta != 0)
|
||||||
|
},
|
||||||
|
ACTION_AppInfo: func() error {
|
||||||
|
return o.requireFields("packageName", o.PackageName != "")
|
||||||
|
},
|
||||||
|
ACTION_AppClear: func() error {
|
||||||
|
return o.requireFields("packageName", o.PackageName != "")
|
||||||
|
},
|
||||||
|
ACTION_AppLaunch: func() error {
|
||||||
|
return o.requireFields("packageName", o.PackageName != "")
|
||||||
|
},
|
||||||
|
ACTION_AppTerminate: func() error {
|
||||||
|
return o.requireFields("packageName", o.PackageName != "")
|
||||||
|
},
|
||||||
|
ACTION_AppUninstall: func() error {
|
||||||
|
return o.requireFields("packageName", o.PackageName != "")
|
||||||
|
},
|
||||||
|
ACTION_AppInstall: func() error {
|
||||||
|
return o.requireFields("appUrl", o.AppUrl != "")
|
||||||
|
},
|
||||||
|
ACTION_TapByOCR: func() error {
|
||||||
|
return o.requireFields("text", o.Text != "")
|
||||||
|
},
|
||||||
|
ACTION_SwipeToTapText: func() error {
|
||||||
|
return o.requireFields("text", o.Text != "")
|
||||||
|
},
|
||||||
|
ACTION_TapByCV: func() error {
|
||||||
|
return o.requireFields("imagePath", o.ImagePath != "")
|
||||||
|
},
|
||||||
|
ACTION_SwipeToTapApp: func() error {
|
||||||
|
return o.requireFields("appName", o.AppName != "")
|
||||||
|
},
|
||||||
|
ACTION_SwipeToTapTexts: func() error {
|
||||||
|
return o.requireFields("texts array", len(o.Texts) > 0)
|
||||||
|
},
|
||||||
|
ACTION_TapBySelector: func() error {
|
||||||
|
return o.requireFields("selector", o.Selector != "")
|
||||||
|
},
|
||||||
|
ACTION_HoverBySelector: func() error {
|
||||||
|
return o.requireFields("selector", o.Selector != "")
|
||||||
|
},
|
||||||
|
ACTION_SecondaryClickBySelector: func() error {
|
||||||
|
return o.requireFields("selector", o.Selector != "")
|
||||||
|
},
|
||||||
|
ACTION_WebCloseTab: func() error {
|
||||||
|
return o.requireFields("tabIndex", o.TabIndex != 0)
|
||||||
|
},
|
||||||
|
ACTION_WebLoginNoneUI: func() error {
|
||||||
|
if o.PackageName == "" || o.PhoneNumber == "" || o.Captcha == "" || o.Password == "" {
|
||||||
|
return fmt.Errorf("packageName, phoneNumber, captcha, and password are required for web_login_none_ui action")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ACTION_SetIme: func() error {
|
||||||
|
return o.requireFields("ime", o.Ime != "")
|
||||||
|
},
|
||||||
|
ACTION_GetSource: func() error {
|
||||||
|
return o.requireFields("packageName", o.PackageName != "")
|
||||||
|
},
|
||||||
|
ACTION_SleepMS: func() error {
|
||||||
|
return o.requireFields("milliseconds", o.Milliseconds != 0)
|
||||||
|
},
|
||||||
|
ACTION_SleepRandom: func() error {
|
||||||
|
return o.requireFields("params array", len(o.Params) > 0)
|
||||||
|
},
|
||||||
|
ACTION_AIAction: func() error {
|
||||||
|
return o.requireFields("prompt", o.Prompt != "")
|
||||||
|
},
|
||||||
|
ACTION_Finished: func() error {
|
||||||
|
return o.requireFields("content", o.Content != "")
|
||||||
|
},
|
||||||
|
ACTION_Upload: func() error {
|
||||||
|
if o.X == 0 || o.Y == 0 || o.FileUrl == "" {
|
||||||
|
return fmt.Errorf("x, y coordinates and fileUrl are required for upload action")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ACTION_PushMedia: func() error {
|
||||||
|
if o.ImageUrl == "" && o.VideoUrl == "" {
|
||||||
|
return fmt.Errorf("either imageUrl or videoUrl is required for push_media action")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ACTION_CreateBrowser: func() error {
|
||||||
|
return o.requireFields("timeout", o.Timeout != 0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute validation rule for the action type
|
||||||
|
if validator, exists := validationRules[actionType]; exists {
|
||||||
|
return validator()
|
||||||
|
}
|
||||||
|
|
||||||
|
// No specific validation needed for this action type
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// requireFields is a helper function to generate consistent error messages
|
||||||
|
func (o *ActionOptions) requireFields(fieldDesc string, condition bool) error {
|
||||||
|
if !condition {
|
||||||
|
return fmt.Errorf("%s is required for this action", fieldDesc)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMCPOptions generates MCP tool options for specific action types
|
||||||
|
func (o *ActionOptions) GetMCPOptions(actionType ActionName) []mcp.ToolOption {
|
||||||
|
// Define field mappings for different action types
|
||||||
|
fieldMappings := map[ActionName][]string{
|
||||||
|
ACTION_TapXY: {"platform", "serial", "x", "y", "duration"},
|
||||||
|
ACTION_TapAbsXY: {"platform", "serial", "x", "y", "duration"},
|
||||||
|
ACTION_TapByOCR: {"platform", "serial", "text", "ignoreNotFoundError", "maxRetryTimes", "index", "regex", "tapRandomRect"},
|
||||||
|
ACTION_TapByCV: {"platform", "serial", "ignoreNotFoundError", "maxRetryTimes", "index", "tapRandomRect"},
|
||||||
|
ACTION_DoubleTapXY: {"platform", "serial", "x", "y"},
|
||||||
|
ACTION_SwipeDirection: {"platform", "serial", "direction", "duration", "pressDuration"},
|
||||||
|
ACTION_SwipeCoordinate: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"},
|
||||||
|
ACTION_Swipe: {"platform", "serial", "direction", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"},
|
||||||
|
ACTION_Drag: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"},
|
||||||
|
ACTION_Input: {"platform", "serial", "text", "frequency"},
|
||||||
|
ACTION_AppLaunch: {"platform", "serial", "packageName"},
|
||||||
|
ACTION_AppTerminate: {"platform", "serial", "packageName"},
|
||||||
|
ACTION_AppInstall: {"platform", "serial", "appUrl", "packageName"},
|
||||||
|
ACTION_AppUninstall: {"platform", "serial", "packageName"},
|
||||||
|
ACTION_AppClear: {"platform", "serial", "packageName"},
|
||||||
|
ACTION_PressButton: {"platform", "serial", "button"},
|
||||||
|
ACTION_SwipeToTapApp: {"platform", "serial", "appName", "ignoreNotFoundError", "maxRetryTimes", "index"},
|
||||||
|
ACTION_SwipeToTapText: {"platform", "serial", "text", "ignoreNotFoundError", "maxRetryTimes", "index", "regex"},
|
||||||
|
ACTION_SwipeToTapTexts: {"platform", "serial", "texts", "ignoreNotFoundError", "maxRetryTimes", "index", "regex"},
|
||||||
|
ACTION_SecondaryClick: {"platform", "serial", "x", "y"},
|
||||||
|
ACTION_HoverBySelector: {"platform", "serial", "selector"},
|
||||||
|
ACTION_TapBySelector: {"platform", "serial", "selector"},
|
||||||
|
ACTION_SecondaryClickBySelector: {"platform", "serial", "selector"},
|
||||||
|
ACTION_WebCloseTab: {"platform", "serial", "tabIndex"},
|
||||||
|
ACTION_WebLoginNoneUI: {"platform", "serial", "packageName", "phoneNumber", "captcha", "password"},
|
||||||
|
ACTION_SetIme: {"platform", "serial", "ime"},
|
||||||
|
ACTION_GetSource: {"platform", "serial", "packageName"},
|
||||||
|
ACTION_Sleep: {"seconds"},
|
||||||
|
ACTION_SleepMS: {"platform", "serial", "milliseconds"},
|
||||||
|
ACTION_SleepRandom: {"platform", "serial", "params"},
|
||||||
|
ACTION_AIAction: {"platform", "serial", "prompt"},
|
||||||
|
ACTION_Finished: {"content"},
|
||||||
|
ACTION_ListAvailableDevices: {},
|
||||||
|
ACTION_SelectDevice: {"platform", "serial"},
|
||||||
|
ACTION_ScreenShot: {"platform", "serial"},
|
||||||
|
ACTION_GetScreenSize: {"platform", "serial"},
|
||||||
|
ACTION_Home: {"platform", "serial"},
|
||||||
|
ACTION_Back: {"platform", "serial"},
|
||||||
|
ACTION_ListPackages: {"platform", "serial"},
|
||||||
|
ACTION_ClosePopups: {"platform", "serial"},
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := fieldMappings[actionType]
|
||||||
|
// Generate options for specified fields, or all fields if not mapped
|
||||||
|
return o.generateMCPOptionsForFields(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateMCPOptionsForFields generates MCP options for specific fields
|
||||||
|
func (o *ActionOptions) generateMCPOptionsForFields(fields []string) []mcp.ToolOption {
|
||||||
|
options := make([]mcp.ToolOption, 0)
|
||||||
|
|
||||||
|
// If no fields are specified, return empty options (e.g., for ACTION_ListAvailableDevices)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
rType := reflect.TypeOf(*o)
|
||||||
|
|
||||||
|
// Process specific fields
|
||||||
|
fieldMap := make(map[string]reflect.StructField)
|
||||||
|
for i := 0; i < rType.NumField(); i++ {
|
||||||
|
field := rType.Field(i)
|
||||||
|
jsonTag := field.Tag.Get("json")
|
||||||
|
if jsonTag != "" && jsonTag != "-" {
|
||||||
|
name := strings.Split(jsonTag, ",")[0]
|
||||||
|
fieldMap[name] = field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fieldName := range fields {
|
||||||
|
field, exists := fieldMap[fieldName]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonTag := field.Tag.Get("json")
|
||||||
|
if jsonTag == "" || jsonTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.Split(jsonTag, ",")[0]
|
||||||
|
binding := field.Tag.Get("binding")
|
||||||
|
required := strings.Contains(binding, "required")
|
||||||
|
desc := field.Tag.Get("desc")
|
||||||
|
|
||||||
|
// Handle pointer types
|
||||||
|
fieldType := field.Type
|
||||||
|
if fieldType.Kind() == reflect.Ptr {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fieldType.Kind() {
|
||||||
|
case reflect.Float64, reflect.Float32, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if required {
|
||||||
|
options = append(options, mcp.WithNumber(name, mcp.Required(), mcp.Description(desc)))
|
||||||
|
} else {
|
||||||
|
options = append(options, mcp.WithNumber(name, mcp.Description(desc)))
|
||||||
|
}
|
||||||
|
case reflect.String:
|
||||||
|
if required {
|
||||||
|
options = append(options, mcp.WithString(name, mcp.Required(), mcp.Description(desc)))
|
||||||
|
} else {
|
||||||
|
options = append(options, mcp.WithString(name, mcp.Description(desc)))
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
if required {
|
||||||
|
options = append(options, mcp.WithBoolean(name, mcp.Required(), mcp.Description(desc)))
|
||||||
|
} else {
|
||||||
|
options = append(options, mcp.WithBoolean(name, mcp.Description(desc)))
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
if fieldType.Elem().Kind() == reflect.String || fieldType.Elem().Kind() == reflect.Float64 {
|
||||||
|
if required {
|
||||||
|
options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc)))
|
||||||
|
} else {
|
||||||
|
options = append(options, mcp.WithArray(name, mcp.Description(desc)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Map, reflect.Interface:
|
||||||
|
// Skip map and interface types for now
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
log.Warn().Str("field_type", fieldType.String()).Msg("Unsupported field type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnifiedActionRequest_Options(t *testing.T) {
|
||||||
|
// Test TapXY request conversion
|
||||||
|
unifiedReq := &ActionOptions{
|
||||||
|
Platform: "android",
|
||||||
|
Serial: "device123",
|
||||||
|
X: 0.5,
|
||||||
|
Y: 0.7,
|
||||||
|
Duration: 1.0,
|
||||||
|
MaxRetryTimes: 3,
|
||||||
|
ScreenOptions: ScreenOptions{
|
||||||
|
ScreenFilterOptions: ScreenFilterOptions{
|
||||||
|
Regex: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
actionOpts := unifiedReq.Options()
|
||||||
|
|
||||||
|
assert.Equal(t, 1.0, unifiedReq.Duration)
|
||||||
|
assert.Equal(t, 3, unifiedReq.MaxRetryTimes)
|
||||||
|
assert.True(t, unifiedReq.Regex)
|
||||||
|
assert.NotEmpty(t, actionOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedActionRequest_GetMCPOptions(t *testing.T) {
|
||||||
|
unifiedReq := &ActionOptions{
|
||||||
|
Platform: "android",
|
||||||
|
Serial: "device123",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test TapXY options
|
||||||
|
tapOptions := unifiedReq.GetMCPOptions(ACTION_TapXY)
|
||||||
|
assert.NotEmpty(t, tapOptions)
|
||||||
|
|
||||||
|
// Test TapByOCR options
|
||||||
|
ocrOptions := unifiedReq.GetMCPOptions(ACTION_TapByOCR)
|
||||||
|
assert.NotEmpty(t, ocrOptions)
|
||||||
|
|
||||||
|
// Test unknown action (should return empty options)
|
||||||
|
unknownOptions := unifiedReq.GetMCPOptions("unknown_action")
|
||||||
|
assert.Empty(t, unknownOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedActionRequest_SwipeDirection(t *testing.T) {
|
||||||
|
unifiedReq := &ActionOptions{
|
||||||
|
Platform: "android",
|
||||||
|
Serial: "device123",
|
||||||
|
Direction: "up",
|
||||||
|
Duration: 2.0,
|
||||||
|
PressDuration: 0.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := unifiedReq.Options()
|
||||||
|
assert.Equal(t, "up", unifiedReq.Direction)
|
||||||
|
assert.Equal(t, 2.0, unifiedReq.Duration)
|
||||||
|
assert.Equal(t, 0.5, unifiedReq.PressDuration)
|
||||||
|
assert.NotEmpty(t, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedActionRequest_SwipeCoordinate(t *testing.T) {
|
||||||
|
params := []float64{0.2, 0.8, 0.2, 0.2}
|
||||||
|
|
||||||
|
unifiedReq := &ActionOptions{
|
||||||
|
Platform: "android",
|
||||||
|
Serial: "device123",
|
||||||
|
Direction: params,
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := unifiedReq.Options()
|
||||||
|
assert.Equal(t, params, unifiedReq.Direction)
|
||||||
|
assert.NotEmpty(t, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedActionRequest_ScreenOptions(t *testing.T) {
|
||||||
|
uiTypes := []string{"button", "text"}
|
||||||
|
|
||||||
|
unifiedReq := &ActionOptions{
|
||||||
|
Platform: "android",
|
||||||
|
Serial: "device123",
|
||||||
|
ScreenOptions: ScreenOptions{
|
||||||
|
ScreenShotOptions: ScreenShotOptions{
|
||||||
|
ScreenShotWithOCR: true,
|
||||||
|
ScreenShotWithUpload: true,
|
||||||
|
ScreenShotWithUITypes: uiTypes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := unifiedReq.Options()
|
||||||
|
assert.True(t, unifiedReq.ScreenShotWithOCR)
|
||||||
|
assert.True(t, unifiedReq.ScreenShotWithUpload)
|
||||||
|
assert.Equal(t, uiTypes, unifiedReq.ScreenShotWithUITypes)
|
||||||
|
assert.NotEmpty(t, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedActionRequest_NilPointerSafety(t *testing.T) {
|
||||||
|
// Test with nil pointers
|
||||||
|
unifiedReq := &ActionOptions{
|
||||||
|
Platform: "android",
|
||||||
|
Serial: "device123",
|
||||||
|
// All pointer fields are nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := unifiedReq.Options()
|
||||||
|
assert.Equal(t, 0, unifiedReq.MaxRetryTimes)
|
||||||
|
assert.Equal(t, 0.0, unifiedReq.Duration)
|
||||||
|
assert.Equal(t, 0.0, unifiedReq.PressDuration)
|
||||||
|
assert.False(t, unifiedReq.Regex)
|
||||||
|
assert.False(t, unifiedReq.TapRandomRect)
|
||||||
|
// When all fields are default values, Options() may return empty slice
|
||||||
|
// This is expected behavior
|
||||||
|
assert.NotNil(t, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedActionRequest_CustomOptions(t *testing.T) {
|
||||||
|
customData := map[string]interface{}{
|
||||||
|
"custom_key": "custom_value",
|
||||||
|
"number": 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
unifiedReq := &ActionOptions{
|
||||||
|
Platform: "android",
|
||||||
|
Serial: "device123",
|
||||||
|
Custom: customData,
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := unifiedReq.Options()
|
||||||
|
assert.Equal(t, customData, unifiedReq.Custom)
|
||||||
|
assert.NotEmpty(t, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedActionRequest_BasicTypeFields(t *testing.T) {
|
||||||
|
// Test basic type fields (no longer pointers)
|
||||||
|
unifiedReq := &ActionOptions{
|
||||||
|
Platform: "android",
|
||||||
|
Serial: "device123",
|
||||||
|
Count: 5,
|
||||||
|
Keycode: 123,
|
||||||
|
Delta: 10,
|
||||||
|
Width: 800,
|
||||||
|
Height: 600,
|
||||||
|
Seconds: 2.5,
|
||||||
|
Milliseconds: 1500,
|
||||||
|
TabIndex: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test direct field access (no need for Getter methods)
|
||||||
|
assert.Equal(t, 5, unifiedReq.Count)
|
||||||
|
assert.Equal(t, 123, unifiedReq.Keycode)
|
||||||
|
assert.Equal(t, 10, unifiedReq.Delta)
|
||||||
|
assert.Equal(t, 800, unifiedReq.Width)
|
||||||
|
assert.Equal(t, 600, unifiedReq.Height)
|
||||||
|
assert.Equal(t, 2.5, unifiedReq.Seconds)
|
||||||
|
assert.Equal(t, int64(1500), unifiedReq.Milliseconds)
|
||||||
|
assert.Equal(t, 3, unifiedReq.TabIndex)
|
||||||
|
|
||||||
|
// Test zero value detection
|
||||||
|
emptyReq := &ActionOptions{}
|
||||||
|
assert.Equal(t, 0, emptyReq.Count)
|
||||||
|
assert.Equal(t, 0, emptyReq.Keycode)
|
||||||
|
assert.Equal(t, 0, emptyReq.Delta)
|
||||||
|
assert.Equal(t, 0, emptyReq.Width)
|
||||||
|
assert.Equal(t, 0, emptyReq.Height)
|
||||||
|
assert.Equal(t, 0.0, emptyReq.Seconds)
|
||||||
|
assert.Equal(t, int64(0), emptyReq.Milliseconds)
|
||||||
|
assert.Equal(t, 0, emptyReq.TabIndex)
|
||||||
|
}
|
||||||
@@ -1,752 +0,0 @@
|
|||||||
package option
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewMCPOptions creates MCP tool options from a struct using reflection
|
|
||||||
// This function is kept for backward compatibility with existing code
|
|
||||||
// New code should use UnifiedActionRequest.GetMCPOptions() instead
|
|
||||||
func NewMCPOptions(t interface{}) (options []mcp.ToolOption) {
|
|
||||||
tType := reflect.TypeOf(t)
|
|
||||||
|
|
||||||
// Handle pointer type by getting the element type
|
|
||||||
if tType.Kind() == reflect.Ptr {
|
|
||||||
tType = tType.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have a struct type
|
|
||||||
if tType.Kind() != reflect.Struct {
|
|
||||||
log.Warn().Str("type", tType.String()).Msg("NewMCPOptions expects a struct or pointer to struct")
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < tType.NumField(); i++ {
|
|
||||||
field := tType.Field(i)
|
|
||||||
jsonTag := field.Tag.Get("json")
|
|
||||||
if jsonTag == "" || jsonTag == "-" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := strings.Split(jsonTag, ",")[0]
|
|
||||||
binding := field.Tag.Get("binding")
|
|
||||||
required := strings.Contains(binding, "required")
|
|
||||||
desc := field.Tag.Get("desc")
|
|
||||||
fieldType := field.Type
|
|
||||||
// Handle pointer types
|
|
||||||
if fieldType.Kind() == reflect.Ptr {
|
|
||||||
fieldType = fieldType.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch fieldType.Kind() {
|
|
||||||
case reflect.Float64, reflect.Float32, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithNumber(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithNumber(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
case reflect.String:
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithString(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithString(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
case reflect.Bool:
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithBoolean(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithBoolean(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
case reflect.Slice:
|
|
||||||
// Handle slice types, especially []string and []float64
|
|
||||||
if field.Type.Elem().Kind() == reflect.String {
|
|
||||||
// Array of strings
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithArray(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
} else if field.Type.Elem().Kind() == reflect.Float64 {
|
|
||||||
// Array of numbers
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithArray(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Warn().Str("field_type", field.Type.String()).Msg("Unsupported field type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnifiedActionRequest represents a unified request structure that combines
|
|
||||||
// ActionOptions with specific action parameters
|
|
||||||
type UnifiedActionRequest struct {
|
|
||||||
// Device targeting
|
|
||||||
Platform string `json:"platform" binding:"omitempty" desc:"Device platform: android/ios/browser"`
|
|
||||||
Serial string `json:"serial" binding:"omitempty" desc:"Device serial/udid/browser id"`
|
|
||||||
|
|
||||||
// Common action parameters
|
|
||||||
X *float64 `json:"x,omitempty" binding:"omitempty,min=0" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
|
||||||
Y *float64 `json:"y,omitempty" binding:"omitempty,min=0" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
|
||||||
FromX *float64 `json:"from_x,omitempty" binding:"omitempty,min=0" desc:"Starting X coordinate"`
|
|
||||||
FromY *float64 `json:"from_y,omitempty" binding:"omitempty,min=0" desc:"Starting Y coordinate"`
|
|
||||||
ToX *float64 `json:"to_x,omitempty" binding:"omitempty,min=0" desc:"Ending X coordinate"`
|
|
||||||
ToY *float64 `json:"to_y,omitempty" binding:"omitempty,min=0" desc:"Ending Y coordinate"`
|
|
||||||
Text string `json:"text,omitempty" desc:"Text content for input/search operations"`
|
|
||||||
Direction string `json:"direction,omitempty" desc:"Direction for swipe operations: up/down/left/right"`
|
|
||||||
|
|
||||||
// App/Package related
|
|
||||||
PackageName string `json:"packageName,omitempty" desc:"Package name of the app"`
|
|
||||||
AppName string `json:"appName,omitempty" desc:"App name to find"`
|
|
||||||
AppUrl string `json:"appUrl,omitempty" desc:"App URL for installation"`
|
|
||||||
MappingUrl string `json:"mappingUrl,omitempty" desc:"Mapping URL for app installation"`
|
|
||||||
ResourceMappingUrl string `json:"resourceMappingUrl,omitempty" desc:"Resource mapping URL for app installation"`
|
|
||||||
|
|
||||||
// Web/Browser related
|
|
||||||
Selector string `json:"selector,omitempty" desc:"CSS or XPath selector"`
|
|
||||||
TabIndex *int `json:"tabIndex,omitempty" desc:"Browser tab index"`
|
|
||||||
PhoneNumber string `json:"phoneNumber,omitempty" desc:"Phone number for login"`
|
|
||||||
Captcha string `json:"captcha,omitempty" desc:"Captcha code"`
|
|
||||||
Password string `json:"password,omitempty" desc:"Password for login"`
|
|
||||||
|
|
||||||
// Button/Key related
|
|
||||||
Button types.DeviceButton `json:"button,omitempty" desc:"Device button to press"`
|
|
||||||
Ime string `json:"ime,omitempty" desc:"IME package name"`
|
|
||||||
Count *int `json:"count,omitempty" desc:"Count for delete operations"`
|
|
||||||
Keycode *int `json:"keycode,omitempty" desc:"Keycode for key press operations"`
|
|
||||||
|
|
||||||
// Image/CV related
|
|
||||||
ImagePath string `json:"imagePath,omitempty" desc:"Path to reference image for CV recognition"`
|
|
||||||
|
|
||||||
// HTTP API specific fields
|
|
||||||
FileUrl string `json:"file_url,omitempty" desc:"File URL for upload operations"`
|
|
||||||
FileFormat string `json:"file_format,omitempty" desc:"File format for upload operations"`
|
|
||||||
ImageUrl string `json:"imageUrl,omitempty" desc:"Image URL for media operations"`
|
|
||||||
VideoUrl string `json:"videoUrl,omitempty" desc:"Video URL for media operations"`
|
|
||||||
Delta *int `json:"delta,omitempty" desc:"Delta value for scroll operations"`
|
|
||||||
Width *int `json:"width,omitempty" desc:"Width for browser creation"`
|
|
||||||
Height *int `json:"height,omitempty" desc:"Height for browser creation"`
|
|
||||||
|
|
||||||
// Array parameters
|
|
||||||
Texts []string `json:"texts,omitempty" desc:"List of texts to search"`
|
|
||||||
Params []float64 `json:"params,omitempty" desc:"Generic parameter array"`
|
|
||||||
|
|
||||||
// AI related
|
|
||||||
Prompt string `json:"prompt,omitempty" desc:"AI action prompt"`
|
|
||||||
Content string `json:"content,omitempty" desc:"Content for finished action"`
|
|
||||||
|
|
||||||
// Time related
|
|
||||||
Seconds *float64 `json:"seconds,omitempty" desc:"Sleep duration in seconds"`
|
|
||||||
Milliseconds *int64 `json:"milliseconds,omitempty" desc:"Sleep duration in milliseconds"`
|
|
||||||
|
|
||||||
// Control options (from ActionOptions)
|
|
||||||
Context context.Context `json:"-" yaml:"-"`
|
|
||||||
Identifier string `json:"identifier,omitempty" desc:"Action identifier for logging"`
|
|
||||||
MaxRetryTimes *int `json:"max_retry_times,omitempty" desc:"Maximum retry times"`
|
|
||||||
Interval *float64 `json:"interval,omitempty" desc:"Interval between retries in seconds"`
|
|
||||||
Duration *float64 `json:"duration,omitempty" desc:"Action duration in seconds"`
|
|
||||||
PressDuration *float64 `json:"press_duration,omitempty" desc:"Press duration in seconds"`
|
|
||||||
Steps *int `json:"steps,omitempty" desc:"Number of steps for action"`
|
|
||||||
Timeout *int `json:"timeout,omitempty" desc:"Timeout in seconds"`
|
|
||||||
Frequency *int `json:"frequency,omitempty" desc:"Action frequency"`
|
|
||||||
|
|
||||||
// Filter options (from ScreenFilterOptions)
|
|
||||||
Scope []float64 `json:"scope,omitempty" desc:"Screen scope [x1,y1,x2,y2] in percentage"`
|
|
||||||
AbsScope []int `json:"absScope,omitempty" desc:"Absolute screen scope [x1,y1,x2,y2] in pixels"`
|
|
||||||
Regex *bool `json:"regex,omitempty" desc:"Use regex to match text"`
|
|
||||||
TapOffset []int `json:"tap_offset,omitempty" desc:"Tap offset [x,y]"`
|
|
||||||
TapRandomRect *bool `json:"tap_random_rect,omitempty" desc:"Tap random point in rectangle"`
|
|
||||||
SwipeOffset []int `json:"swipe_offset,omitempty" desc:"Swipe offset [fromX,fromY,toX,toY]"`
|
|
||||||
OffsetRandomRange []int `json:"offset_random_range,omitempty" desc:"Random offset range [min,max]"`
|
|
||||||
Index *int `json:"index,omitempty" desc:"Element index when multiple matches found"`
|
|
||||||
MatchOne *bool `json:"match_one,omitempty" desc:"Match only one element"`
|
|
||||||
IgnoreNotFoundError *bool `json:"ignore_NotFoundError,omitempty" desc:"Ignore error if element not found"`
|
|
||||||
|
|
||||||
// Screenshot options (from ScreenShotOptions)
|
|
||||||
ScreenShotWithOCR *bool `json:"screenshot_with_ocr,omitempty" desc:"Take screenshot with OCR"`
|
|
||||||
ScreenShotWithUpload *bool `json:"screenshot_with_upload,omitempty" desc:"Upload screenshot"`
|
|
||||||
ScreenShotWithLiveType *bool `json:"screenshot_with_live_type,omitempty" desc:"Screenshot with live type"`
|
|
||||||
ScreenShotWithLivePopularity *bool `json:"screenshot_with_live_popularity,omitempty" desc:"Screenshot with live popularity"`
|
|
||||||
ScreenShotWithUITypes []string `json:"screenshot_with_ui_types,omitempty" desc:"Screenshot with UI types"`
|
|
||||||
ScreenShotWithClosePopups *bool `json:"screenshot_with_close_popups,omitempty" desc:"Close popups before screenshot"`
|
|
||||||
ScreenShotWithOCRCluster string `json:"screenshot_with_ocr_cluster,omitempty" desc:"OCR cluster for screenshot"`
|
|
||||||
ScreenShotFileName string `json:"screenshot_file_name,omitempty" desc:"Screenshot file name"`
|
|
||||||
|
|
||||||
// Screen record options (from ScreenRecordOptions)
|
|
||||||
ScreenRecordDuration *float64 `json:"screenrecord_duration,omitempty" desc:"Screen record duration"`
|
|
||||||
ScreenRecordWithAudio *bool `json:"screenrecord_with_audio,omitempty" desc:"Record with audio"`
|
|
||||||
ScreenRecordWithScrcpy *bool `json:"screenrecord_with_scrcpy,omitempty" desc:"Use scrcpy for recording"`
|
|
||||||
ScreenRecordPath string `json:"screenrecord_path,omitempty" desc:"Screen record output path"`
|
|
||||||
|
|
||||||
// Mark operation options (from MarkOperationOptions)
|
|
||||||
PreMarkOperation *bool `json:"pre_mark_operation,omitempty" desc:"Mark operation before action"`
|
|
||||||
PostMarkOperation *bool `json:"post_mark_operation,omitempty" desc:"Mark operation after action"`
|
|
||||||
|
|
||||||
// Custom options
|
|
||||||
Custom map[string]interface{} `json:"custom,omitempty" desc:"Custom options"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP API direct usage methods
|
|
||||||
|
|
||||||
// GetX returns the X coordinate value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetX() float64 {
|
|
||||||
if r.X != nil {
|
|
||||||
return *r.X
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetY returns the Y coordinate value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetY() float64 {
|
|
||||||
if r.Y != nil {
|
|
||||||
return *r.Y
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFromX returns the FromX coordinate value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetFromX() float64 {
|
|
||||||
if r.FromX != nil {
|
|
||||||
return *r.FromX
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFromY returns the FromY coordinate value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetFromY() float64 {
|
|
||||||
if r.FromY != nil {
|
|
||||||
return *r.FromY
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetToX returns the ToX coordinate value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetToX() float64 {
|
|
||||||
if r.ToX != nil {
|
|
||||||
return *r.ToX
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetToY returns the ToY coordinate value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetToY() float64 {
|
|
||||||
if r.ToY != nil {
|
|
||||||
return *r.ToY
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDuration returns the duration value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetDuration() float64 {
|
|
||||||
if r.Duration != nil {
|
|
||||||
return *r.Duration
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPressDuration returns the press duration value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetPressDuration() float64 {
|
|
||||||
if r.PressDuration != nil {
|
|
||||||
return *r.PressDuration
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCount returns the count value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetCount() int {
|
|
||||||
if r.Count != nil {
|
|
||||||
return *r.Count
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetKeycode returns the keycode value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetKeycode() int {
|
|
||||||
if r.Keycode != nil {
|
|
||||||
return *r.Keycode
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFrequency returns the frequency value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetFrequency() int {
|
|
||||||
if r.Frequency != nil {
|
|
||||||
return *r.Frequency
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTabIndex returns the tab index value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetTabIndex() int {
|
|
||||||
if r.TabIndex != nil {
|
|
||||||
return *r.TabIndex
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDelta returns the delta value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetDelta() int {
|
|
||||||
if r.Delta != nil {
|
|
||||||
return *r.Delta
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWidth returns the width value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetWidth() int {
|
|
||||||
if r.Width != nil {
|
|
||||||
return *r.Width
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeight returns the height value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetHeight() int {
|
|
||||||
if r.Height != nil {
|
|
||||||
return *r.Height
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTimeout returns the timeout value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetTimeout() int {
|
|
||||||
if r.Timeout != nil {
|
|
||||||
return *r.Timeout
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMilliseconds returns the milliseconds value, handling nil pointer safely
|
|
||||||
func (r *UnifiedActionRequest) GetMilliseconds() int64 {
|
|
||||||
if r.Milliseconds != nil {
|
|
||||||
return *r.Milliseconds
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateForHTTPAPI validates the request for HTTP API usage
|
|
||||||
func (r *UnifiedActionRequest) ValidateForHTTPAPI(actionType ActionMethod) error {
|
|
||||||
// Basic validation - Platform and Serial are set from URL, so skip here
|
|
||||||
// They will be validated by setRequestContextFromURL
|
|
||||||
|
|
||||||
// Action-specific validation using a more efficient approach
|
|
||||||
return r.validateActionSpecificFields(actionType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateActionSpecificFields performs action-specific field validation
|
|
||||||
func (r *UnifiedActionRequest) validateActionSpecificFields(actionType ActionMethod) error {
|
|
||||||
// Define validation rules for each action type using ActionMethod constants
|
|
||||||
validationRules := map[ActionMethod]func() error{
|
|
||||||
ACTION_Tap: func() error {
|
|
||||||
return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil)
|
|
||||||
},
|
|
||||||
ACTION_TapXY: func() error {
|
|
||||||
return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil)
|
|
||||||
},
|
|
||||||
ACTION_TapAbsXY: func() error {
|
|
||||||
return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil)
|
|
||||||
},
|
|
||||||
ACTION_DoubleTap: func() error {
|
|
||||||
return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil)
|
|
||||||
},
|
|
||||||
ACTION_DoubleTapXY: func() error {
|
|
||||||
return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil)
|
|
||||||
},
|
|
||||||
ACTION_RightClick: func() error {
|
|
||||||
return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil)
|
|
||||||
},
|
|
||||||
ACTION_SecondaryClick: func() error {
|
|
||||||
return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil)
|
|
||||||
},
|
|
||||||
ACTION_Hover: func() error {
|
|
||||||
return r.requireFields("x and y coordinates", r.X != nil && r.Y != nil)
|
|
||||||
},
|
|
||||||
ACTION_Drag: func() error {
|
|
||||||
return r.requireFields("fromX, fromY, toX, toY coordinates",
|
|
||||||
r.FromX != nil && r.FromY != nil && r.ToX != nil && r.ToY != nil)
|
|
||||||
},
|
|
||||||
ACTION_SwipeCoordinate: func() error {
|
|
||||||
return r.requireFields("fromX, fromY, toX, toY coordinates",
|
|
||||||
r.FromX != nil && r.FromY != nil && r.ToX != nil && r.ToY != nil)
|
|
||||||
},
|
|
||||||
ACTION_Swipe: func() error {
|
|
||||||
return r.requireFields("direction", r.Direction != "")
|
|
||||||
},
|
|
||||||
ACTION_SwipeDirection: func() error {
|
|
||||||
return r.requireFields("direction", r.Direction != "")
|
|
||||||
},
|
|
||||||
ACTION_Input: func() error {
|
|
||||||
return r.requireFields("text", r.Text != "")
|
|
||||||
},
|
|
||||||
ACTION_Delete: func() error {
|
|
||||||
// Count is optional, will use default if not provided
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ACTION_Backspace: func() error {
|
|
||||||
// Count is optional, will use default if not provided
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ACTION_KeyCode: func() error {
|
|
||||||
return r.requireFields("keycode", r.Keycode != nil)
|
|
||||||
},
|
|
||||||
ACTION_Scroll: func() error {
|
|
||||||
return r.requireFields("delta", r.Delta != nil)
|
|
||||||
},
|
|
||||||
ACTION_AppInfo: func() error {
|
|
||||||
return r.requireFields("packageName", r.PackageName != "")
|
|
||||||
},
|
|
||||||
ACTION_AppClear: func() error {
|
|
||||||
return r.requireFields("packageName", r.PackageName != "")
|
|
||||||
},
|
|
||||||
ACTION_AppLaunch: func() error {
|
|
||||||
return r.requireFields("packageName", r.PackageName != "")
|
|
||||||
},
|
|
||||||
ACTION_AppTerminate: func() error {
|
|
||||||
return r.requireFields("packageName", r.PackageName != "")
|
|
||||||
},
|
|
||||||
ACTION_AppUninstall: func() error {
|
|
||||||
return r.requireFields("packageName", r.PackageName != "")
|
|
||||||
},
|
|
||||||
ACTION_AppInstall: func() error {
|
|
||||||
return r.requireFields("appUrl", r.AppUrl != "")
|
|
||||||
},
|
|
||||||
ACTION_TapByOCR: func() error {
|
|
||||||
return r.requireFields("text", r.Text != "")
|
|
||||||
},
|
|
||||||
ACTION_SwipeToTapText: func() error {
|
|
||||||
return r.requireFields("text", r.Text != "")
|
|
||||||
},
|
|
||||||
ACTION_TapByCV: func() error {
|
|
||||||
return r.requireFields("imagePath", r.ImagePath != "")
|
|
||||||
},
|
|
||||||
ACTION_SwipeToTapApp: func() error {
|
|
||||||
return r.requireFields("appName", r.AppName != "")
|
|
||||||
},
|
|
||||||
ACTION_SwipeToTapTexts: func() error {
|
|
||||||
return r.requireFields("texts array", len(r.Texts) > 0)
|
|
||||||
},
|
|
||||||
ACTION_TapBySelector: func() error {
|
|
||||||
return r.requireFields("selector", r.Selector != "")
|
|
||||||
},
|
|
||||||
ACTION_HoverBySelector: func() error {
|
|
||||||
return r.requireFields("selector", r.Selector != "")
|
|
||||||
},
|
|
||||||
ACTION_SecondaryClickBySelector: func() error {
|
|
||||||
return r.requireFields("selector", r.Selector != "")
|
|
||||||
},
|
|
||||||
ACTION_WebCloseTab: func() error {
|
|
||||||
return r.requireFields("tabIndex", r.TabIndex != nil)
|
|
||||||
},
|
|
||||||
ACTION_WebLoginNoneUI: func() error {
|
|
||||||
if r.PackageName == "" || r.PhoneNumber == "" || r.Captcha == "" || r.Password == "" {
|
|
||||||
return fmt.Errorf("packageName, phoneNumber, captcha, and password are required for web_login_none_ui action")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ACTION_SetIme: func() error {
|
|
||||||
return r.requireFields("ime", r.Ime != "")
|
|
||||||
},
|
|
||||||
ACTION_GetSource: func() error {
|
|
||||||
return r.requireFields("packageName", r.PackageName != "")
|
|
||||||
},
|
|
||||||
ACTION_SleepMS: func() error {
|
|
||||||
return r.requireFields("milliseconds", r.Milliseconds != nil)
|
|
||||||
},
|
|
||||||
ACTION_SleepRandom: func() error {
|
|
||||||
return r.requireFields("params array", len(r.Params) > 0)
|
|
||||||
},
|
|
||||||
ACTION_AIAction: func() error {
|
|
||||||
return r.requireFields("prompt", r.Prompt != "")
|
|
||||||
},
|
|
||||||
ACTION_Finished: func() error {
|
|
||||||
return r.requireFields("content", r.Content != "")
|
|
||||||
},
|
|
||||||
ACTION_Upload: func() error {
|
|
||||||
if r.X == nil || r.Y == nil || r.FileUrl == "" {
|
|
||||||
return fmt.Errorf("x, y coordinates and fileUrl are required for upload action")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ACTION_PushMedia: func() error {
|
|
||||||
if r.ImageUrl == "" && r.VideoUrl == "" {
|
|
||||||
return fmt.Errorf("either imageUrl or videoUrl is required for push_media action")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ACTION_CreateBrowser: func() error {
|
|
||||||
return r.requireFields("timeout", r.Timeout != nil)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute validation rule for the action type
|
|
||||||
if validator, exists := validationRules[actionType]; exists {
|
|
||||||
return validator()
|
|
||||||
}
|
|
||||||
|
|
||||||
// No specific validation needed for this action type
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// requireFields is a helper function to generate consistent error messages
|
|
||||||
func (r *UnifiedActionRequest) requireFields(fieldDesc string, condition bool) error {
|
|
||||||
if !condition {
|
|
||||||
return fmt.Errorf("%s is required for this action", fieldDesc)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToActionOptions converts UnifiedActionRequest to ActionOptions
|
|
||||||
func (r *UnifiedActionRequest) ToActionOptions() *ActionOptions {
|
|
||||||
opts := &ActionOptions{
|
|
||||||
Context: r.Context,
|
|
||||||
Identifier: r.Identifier,
|
|
||||||
Custom: r.Custom,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy pointer values safely
|
|
||||||
if r.MaxRetryTimes != nil {
|
|
||||||
opts.MaxRetryTimes = *r.MaxRetryTimes
|
|
||||||
}
|
|
||||||
if r.Interval != nil {
|
|
||||||
opts.Interval = *r.Interval
|
|
||||||
}
|
|
||||||
if r.Duration != nil {
|
|
||||||
opts.Duration = *r.Duration
|
|
||||||
}
|
|
||||||
if r.PressDuration != nil {
|
|
||||||
opts.PressDuration = *r.PressDuration
|
|
||||||
}
|
|
||||||
if r.Steps != nil {
|
|
||||||
opts.Steps = *r.Steps
|
|
||||||
}
|
|
||||||
if r.Timeout != nil {
|
|
||||||
opts.Timeout = *r.Timeout
|
|
||||||
}
|
|
||||||
if r.Frequency != nil {
|
|
||||||
opts.Frequency = *r.Frequency
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle direction
|
|
||||||
if r.Direction != "" {
|
|
||||||
opts.Direction = r.Direction
|
|
||||||
} else if len(r.Params) == 4 {
|
|
||||||
opts.Direction = r.Params
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy filter options (ScreenFilterOptions)
|
|
||||||
opts.ScreenFilterOptions.Scope = r.Scope
|
|
||||||
opts.ScreenFilterOptions.AbsScope = r.AbsScope
|
|
||||||
if r.Regex != nil {
|
|
||||||
opts.ScreenFilterOptions.Regex = *r.Regex
|
|
||||||
}
|
|
||||||
opts.ScreenFilterOptions.TapOffset = r.TapOffset
|
|
||||||
if r.TapRandomRect != nil {
|
|
||||||
opts.ScreenFilterOptions.TapRandomRect = *r.TapRandomRect
|
|
||||||
}
|
|
||||||
opts.ScreenFilterOptions.SwipeOffset = r.SwipeOffset
|
|
||||||
opts.ScreenFilterOptions.OffsetRandomRange = r.OffsetRandomRange
|
|
||||||
if r.Index != nil {
|
|
||||||
opts.ScreenFilterOptions.Index = *r.Index
|
|
||||||
}
|
|
||||||
if r.MatchOne != nil {
|
|
||||||
opts.ScreenFilterOptions.MatchOne = *r.MatchOne
|
|
||||||
}
|
|
||||||
if r.IgnoreNotFoundError != nil {
|
|
||||||
opts.ScreenFilterOptions.IgnoreNotFoundError = *r.IgnoreNotFoundError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy screenshot options (ScreenShotOptions)
|
|
||||||
if r.ScreenShotWithOCR != nil {
|
|
||||||
opts.ScreenShotOptions.ScreenShotWithOCR = *r.ScreenShotWithOCR
|
|
||||||
}
|
|
||||||
if r.ScreenShotWithUpload != nil {
|
|
||||||
opts.ScreenShotOptions.ScreenShotWithUpload = *r.ScreenShotWithUpload
|
|
||||||
}
|
|
||||||
if r.ScreenShotWithLiveType != nil {
|
|
||||||
opts.ScreenShotOptions.ScreenShotWithLiveType = *r.ScreenShotWithLiveType
|
|
||||||
}
|
|
||||||
if r.ScreenShotWithLivePopularity != nil {
|
|
||||||
opts.ScreenShotOptions.ScreenShotWithLivePopularity = *r.ScreenShotWithLivePopularity
|
|
||||||
}
|
|
||||||
opts.ScreenShotOptions.ScreenShotWithUITypes = r.ScreenShotWithUITypes
|
|
||||||
if r.ScreenShotWithClosePopups != nil {
|
|
||||||
opts.ScreenShotOptions.ScreenShotWithClosePopups = *r.ScreenShotWithClosePopups
|
|
||||||
}
|
|
||||||
opts.ScreenShotOptions.ScreenShotWithOCRCluster = r.ScreenShotWithOCRCluster
|
|
||||||
opts.ScreenShotOptions.ScreenShotFileName = r.ScreenShotFileName
|
|
||||||
|
|
||||||
// Copy screen record options (ScreenRecordOptions)
|
|
||||||
if r.ScreenRecordDuration != nil {
|
|
||||||
opts.ScreenRecordOptions.ScreenRecordDuration = *r.ScreenRecordDuration
|
|
||||||
}
|
|
||||||
if r.ScreenRecordWithAudio != nil {
|
|
||||||
opts.ScreenRecordOptions.ScreenRecordWithAudio = *r.ScreenRecordWithAudio
|
|
||||||
}
|
|
||||||
if r.ScreenRecordWithScrcpy != nil {
|
|
||||||
opts.ScreenRecordOptions.ScreenRecordWithScrcpy = *r.ScreenRecordWithScrcpy
|
|
||||||
}
|
|
||||||
opts.ScreenRecordOptions.ScreenRecordPath = r.ScreenRecordPath
|
|
||||||
|
|
||||||
// Copy mark operation options (MarkOperationOptions)
|
|
||||||
if r.PreMarkOperation != nil {
|
|
||||||
opts.MarkOperationOptions.PreMarkOperation = *r.PreMarkOperation
|
|
||||||
}
|
|
||||||
if r.PostMarkOperation != nil {
|
|
||||||
opts.MarkOperationOptions.PostMarkOperation = *r.PostMarkOperation
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMCPOptions generates MCP tool options for specific action types
|
|
||||||
func (r *UnifiedActionRequest) GetMCPOptions(actionType ActionMethod) []mcp.ToolOption {
|
|
||||||
// Define field mappings for different action types
|
|
||||||
fieldMappings := map[ActionMethod][]string{
|
|
||||||
ACTION_TapXY: {"platform", "serial", "x", "y", "duration"},
|
|
||||||
ACTION_TapAbsXY: {"platform", "serial", "x", "y", "duration"},
|
|
||||||
ACTION_TapByOCR: {"platform", "serial", "text", "ignoreNotFoundError", "maxRetryTimes", "index", "regex", "tapRandomRect"},
|
|
||||||
ACTION_TapByCV: {"platform", "serial", "ignoreNotFoundError", "maxRetryTimes", "index", "tapRandomRect"},
|
|
||||||
ACTION_DoubleTapXY: {"platform", "serial", "x", "y"},
|
|
||||||
ACTION_SwipeDirection: {"platform", "serial", "direction", "duration", "pressDuration"},
|
|
||||||
ACTION_SwipeCoordinate: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"},
|
|
||||||
ACTION_Swipe: {"platform", "serial", "direction", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"},
|
|
||||||
ACTION_Drag: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"},
|
|
||||||
ACTION_Input: {"platform", "serial", "text", "frequency"},
|
|
||||||
ACTION_AppLaunch: {"platform", "serial", "packageName"},
|
|
||||||
ACTION_AppTerminate: {"platform", "serial", "packageName"},
|
|
||||||
ACTION_AppInstall: {"platform", "serial", "appUrl", "packageName"},
|
|
||||||
ACTION_AppUninstall: {"platform", "serial", "packageName"},
|
|
||||||
ACTION_AppClear: {"platform", "serial", "packageName"},
|
|
||||||
ACTION_PressButton: {"platform", "serial", "button"},
|
|
||||||
ACTION_SwipeToTapApp: {"platform", "serial", "appName", "ignoreNotFoundError", "maxRetryTimes", "index"},
|
|
||||||
ACTION_SwipeToTapText: {"platform", "serial", "text", "ignoreNotFoundError", "maxRetryTimes", "index", "regex"},
|
|
||||||
ACTION_SwipeToTapTexts: {"platform", "serial", "texts", "ignoreNotFoundError", "maxRetryTimes", "index", "regex"},
|
|
||||||
ACTION_SecondaryClick: {"platform", "serial", "x", "y"},
|
|
||||||
ACTION_HoverBySelector: {"platform", "serial", "selector"},
|
|
||||||
ACTION_TapBySelector: {"platform", "serial", "selector"},
|
|
||||||
ACTION_SecondaryClickBySelector: {"platform", "serial", "selector"},
|
|
||||||
ACTION_WebCloseTab: {"platform", "serial", "tabIndex"},
|
|
||||||
ACTION_WebLoginNoneUI: {"platform", "serial", "packageName", "phoneNumber", "captcha", "password"},
|
|
||||||
ACTION_SetIme: {"platform", "serial", "ime"},
|
|
||||||
ACTION_GetSource: {"platform", "serial", "packageName"},
|
|
||||||
ACTION_Sleep: {"seconds"},
|
|
||||||
ACTION_SleepMS: {"platform", "serial", "milliseconds"},
|
|
||||||
ACTION_SleepRandom: {"platform", "serial", "params"},
|
|
||||||
ACTION_AIAction: {"platform", "serial", "prompt"},
|
|
||||||
ACTION_Finished: {"content"},
|
|
||||||
ACTION_ListAvailableDevices: {},
|
|
||||||
ACTION_SelectDevice: {"platform", "serial"},
|
|
||||||
ACTION_ScreenShot: {"platform", "serial"},
|
|
||||||
ACTION_GetScreenSize: {"platform", "serial"},
|
|
||||||
ACTION_Home: {"platform", "serial"},
|
|
||||||
ACTION_Back: {"platform", "serial"},
|
|
||||||
ACTION_ListPackages: {"platform", "serial"},
|
|
||||||
ACTION_ClosePopups: {"platform", "serial"},
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := fieldMappings[actionType]
|
|
||||||
if fields == nil {
|
|
||||||
// Fallback to all fields if not specifically mapped
|
|
||||||
return NewMCPOptions(*r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate options only for specified fields
|
|
||||||
return r.generateMCPOptionsForFields(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateMCPOptionsForFields generates MCP options for specific fields
|
|
||||||
func (r *UnifiedActionRequest) generateMCPOptionsForFields(fields []string) []mcp.ToolOption {
|
|
||||||
options := make([]mcp.ToolOption, 0)
|
|
||||||
rType := reflect.TypeOf(*r)
|
|
||||||
rValue := reflect.ValueOf(*r)
|
|
||||||
|
|
||||||
fieldMap := make(map[string]reflect.StructField)
|
|
||||||
for i := 0; i < rType.NumField(); i++ {
|
|
||||||
field := rType.Field(i)
|
|
||||||
jsonTag := field.Tag.Get("json")
|
|
||||||
if jsonTag != "" && jsonTag != "-" {
|
|
||||||
name := strings.Split(jsonTag, ",")[0]
|
|
||||||
fieldMap[name] = field
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fieldName := range fields {
|
|
||||||
field, exists := fieldMap[fieldName]
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonTag := field.Tag.Get("json")
|
|
||||||
if jsonTag == "" || jsonTag == "-" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := strings.Split(jsonTag, ",")[0]
|
|
||||||
binding := field.Tag.Get("binding")
|
|
||||||
required := strings.Contains(binding, "required")
|
|
||||||
desc := field.Tag.Get("desc")
|
|
||||||
|
|
||||||
// Check if field has a value
|
|
||||||
fieldValue := rValue.FieldByName(field.Name)
|
|
||||||
if !fieldValue.IsValid() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle pointer types
|
|
||||||
fieldType := field.Type
|
|
||||||
isPointer := false
|
|
||||||
if fieldType.Kind() == reflect.Ptr {
|
|
||||||
isPointer = true
|
|
||||||
fieldType = fieldType.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip nil pointer fields if not required
|
|
||||||
if isPointer && fieldValue.IsNil() && !required {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch fieldType.Kind() {
|
|
||||||
case reflect.Float64, reflect.Float32, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithNumber(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithNumber(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
case reflect.String:
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithString(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithString(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
case reflect.Bool:
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithBoolean(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithBoolean(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
case reflect.Slice:
|
|
||||||
if fieldType.Elem().Kind() == reflect.String || fieldType.Elem().Kind() == reflect.Float64 {
|
|
||||||
if required {
|
|
||||||
options = append(options, mcp.WithArray(name, mcp.Required(), mcp.Description(desc)))
|
|
||||||
} else {
|
|
||||||
options = append(options, mcp.WithArray(name, mcp.Description(desc)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case reflect.Map, reflect.Interface:
|
|
||||||
// Skip map and interface types for now
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
log.Warn().Str("field_type", fieldType.String()).Msg("Unsupported field type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package option
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnifiedActionRequest_ToActionOptions(t *testing.T) {
|
|
||||||
// Test TapXY request conversion
|
|
||||||
x := 0.5
|
|
||||||
y := 0.7
|
|
||||||
duration := 1.0
|
|
||||||
maxRetryTimes := 3
|
|
||||||
regex := true
|
|
||||||
|
|
||||||
unifiedReq := &UnifiedActionRequest{
|
|
||||||
Platform: "android",
|
|
||||||
Serial: "device123",
|
|
||||||
X: &x,
|
|
||||||
Y: &y,
|
|
||||||
Duration: &duration,
|
|
||||||
MaxRetryTimes: &maxRetryTimes,
|
|
||||||
Regex: ®ex,
|
|
||||||
}
|
|
||||||
|
|
||||||
actionOpts := unifiedReq.ToActionOptions()
|
|
||||||
|
|
||||||
assert.Equal(t, 1.0, actionOpts.Duration)
|
|
||||||
assert.Equal(t, 3, actionOpts.MaxRetryTimes)
|
|
||||||
assert.True(t, actionOpts.Regex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnifiedActionRequest_GetMCPOptions(t *testing.T) {
|
|
||||||
unifiedReq := &UnifiedActionRequest{
|
|
||||||
Platform: "android",
|
|
||||||
Serial: "device123",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test TapXY options
|
|
||||||
tapOptions := unifiedReq.GetMCPOptions(ACTION_TapXY)
|
|
||||||
assert.NotEmpty(t, tapOptions)
|
|
||||||
|
|
||||||
// Test TapByOCR options
|
|
||||||
ocrOptions := unifiedReq.GetMCPOptions(ACTION_TapByOCR)
|
|
||||||
assert.NotEmpty(t, ocrOptions)
|
|
||||||
|
|
||||||
// Test unknown action (should fallback to all fields)
|
|
||||||
unknownOptions := unifiedReq.GetMCPOptions("unknown_action")
|
|
||||||
assert.NotEmpty(t, unknownOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnifiedActionRequest_SwipeDirection(t *testing.T) {
|
|
||||||
duration := 2.0
|
|
||||||
pressDuration := 0.5
|
|
||||||
|
|
||||||
unifiedReq := &UnifiedActionRequest{
|
|
||||||
Platform: "android",
|
|
||||||
Serial: "device123",
|
|
||||||
Direction: "up",
|
|
||||||
Duration: &duration,
|
|
||||||
PressDuration: &pressDuration,
|
|
||||||
}
|
|
||||||
|
|
||||||
actionOpts := unifiedReq.ToActionOptions()
|
|
||||||
assert.Equal(t, "up", actionOpts.Direction)
|
|
||||||
assert.Equal(t, 2.0, actionOpts.Duration)
|
|
||||||
assert.Equal(t, 0.5, actionOpts.PressDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnifiedActionRequest_SwipeCoordinate(t *testing.T) {
|
|
||||||
params := []float64{0.2, 0.8, 0.2, 0.2}
|
|
||||||
|
|
||||||
unifiedReq := &UnifiedActionRequest{
|
|
||||||
Platform: "android",
|
|
||||||
Serial: "device123",
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
actionOpts := unifiedReq.ToActionOptions()
|
|
||||||
assert.Equal(t, params, actionOpts.Direction)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnifiedActionRequest_ScreenOptions(t *testing.T) {
|
|
||||||
ocrEnabled := true
|
|
||||||
uploadEnabled := true
|
|
||||||
uiTypes := []string{"button", "text"}
|
|
||||||
|
|
||||||
unifiedReq := &UnifiedActionRequest{
|
|
||||||
Platform: "android",
|
|
||||||
Serial: "device123",
|
|
||||||
ScreenShotWithOCR: &ocrEnabled,
|
|
||||||
ScreenShotWithUpload: &uploadEnabled,
|
|
||||||
ScreenShotWithUITypes: uiTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
actionOpts := unifiedReq.ToActionOptions()
|
|
||||||
assert.True(t, actionOpts.ScreenShotWithOCR)
|
|
||||||
assert.True(t, actionOpts.ScreenShotWithUpload)
|
|
||||||
assert.Equal(t, uiTypes, actionOpts.ScreenShotWithUITypes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnifiedActionRequest_NilPointerSafety(t *testing.T) {
|
|
||||||
// Test with nil pointers
|
|
||||||
unifiedReq := &UnifiedActionRequest{
|
|
||||||
Platform: "android",
|
|
||||||
Serial: "device123",
|
|
||||||
// All pointer fields are nil
|
|
||||||
}
|
|
||||||
|
|
||||||
actionOpts := unifiedReq.ToActionOptions()
|
|
||||||
assert.Equal(t, 0, actionOpts.MaxRetryTimes)
|
|
||||||
assert.Equal(t, 0.0, actionOpts.Duration)
|
|
||||||
assert.Equal(t, 0.0, actionOpts.PressDuration)
|
|
||||||
assert.False(t, actionOpts.Regex)
|
|
||||||
assert.False(t, actionOpts.TapRandomRect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnifiedActionRequest_CustomOptions(t *testing.T) {
|
|
||||||
customData := map[string]interface{}{
|
|
||||||
"custom_key": "custom_value",
|
|
||||||
"number": 42,
|
|
||||||
}
|
|
||||||
|
|
||||||
unifiedReq := &UnifiedActionRequest{
|
|
||||||
Platform: "android",
|
|
||||||
Serial: "device123",
|
|
||||||
Custom: customData,
|
|
||||||
}
|
|
||||||
|
|
||||||
actionOpts := unifiedReq.ToActionOptions()
|
|
||||||
assert.Equal(t, customData, actionOpts.Custom)
|
|
||||||
}
|
|
||||||
+1
-1
@@ -62,7 +62,7 @@ func (c *MCPClient4XTDriver) ListTools(ctx context.Context, req mcp.ListToolsReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *MCPClient4XTDriver) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func (c *MCPClient4XTDriver) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
actionTool := c.Server.GetToolByAction(option.ActionMethod(req.Params.Name))
|
actionTool := c.Server.GetToolByAction(option.ActionName(req.Params.Name))
|
||||||
if actionTool == nil {
|
if actionTool == nil {
|
||||||
return mcp.NewToolResultError(fmt.Sprintf("action %s for tool not found", req.Params.Name)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("action %s for tool not found", req.Params.Name)), nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user