mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-07 00:39:34 +08:00
refactor: move DoAction to MCP tools call
This commit is contained in:
@@ -1,16 +1,6 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/code"
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
@@ -31,261 +21,3 @@ func (ma MobileAction) GetOptions() []option.ActionOption {
|
||||
actionOptionList = append(actionOptionList, ma.ActionOptions.Options()...)
|
||||
return actionOptionList
|
||||
}
|
||||
|
||||
// TODO: merge to uixt MCP Server
|
||||
func (dExt *XTDriver) DoAction(action MobileAction) (err error) {
|
||||
actionStartTime := time.Now()
|
||||
defer func() {
|
||||
var logger *zerolog.Event
|
||||
if err != nil {
|
||||
logger = log.Error().Bool("success", false).Err(err)
|
||||
} else {
|
||||
logger = log.Debug().Bool("success", true)
|
||||
}
|
||||
logger = logger.
|
||||
Str("method", string(action.Method)).
|
||||
Interface("params", action.Params).
|
||||
Int64("elapsed(ms)", time.Since(actionStartTime).Milliseconds())
|
||||
logger.Msg("exec uixt action")
|
||||
}()
|
||||
|
||||
switch action.Method {
|
||||
case option.ACTION_WebLoginNoneUI:
|
||||
if len(action.Params.([]interface{})) == 4 {
|
||||
driver, ok := dExt.IDriver.(*BrowserDriver)
|
||||
if !ok {
|
||||
return errors.New("invalid browser driver")
|
||||
}
|
||||
params := action.Params.([]interface{})
|
||||
_, err = driver.LoginNoneUI(params[0].(string), params[1].(string), params[2].(string), params[3].(string))
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_WebLoginNoneUI, action.Params)
|
||||
case option.ACTION_AppInstall:
|
||||
if app, ok := action.Params.(string); ok {
|
||||
if err = dExt.GetDevice().Install(app,
|
||||
option.WithRetryTimes(action.MaxRetryTimes)); err != nil {
|
||||
return errors.Wrap(err, "failed to install app")
|
||||
}
|
||||
}
|
||||
case option.ACTION_AppUninstall:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
if err = dExt.GetDevice().Uninstall(packageName); err != nil {
|
||||
return errors.Wrap(err, "failed to uninstall app")
|
||||
}
|
||||
}
|
||||
case option.ACTION_AppClear:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
if err = dExt.AppClear(packageName); err != nil {
|
||||
return errors.Wrap(err, "failed to clear app")
|
||||
}
|
||||
}
|
||||
case option.ACTION_AppLaunch:
|
||||
if bundleId, ok := action.Params.(string); ok {
|
||||
return dExt.AppLaunch(bundleId)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params, should be bundleId(string), got %v",
|
||||
option.ACTION_AppLaunch, action.Params)
|
||||
case option.ACTION_SwipeToTapApp:
|
||||
if appName, ok := action.Params.(string); ok {
|
||||
return dExt.SwipeToTapApp(appName, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params, should be app name(string), got %v",
|
||||
option.ACTION_SwipeToTapApp, action.Params)
|
||||
case option.ACTION_SwipeToTapText:
|
||||
if text, ok := action.Params.(string); ok {
|
||||
return dExt.SwipeToTapTexts([]string{text}, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params, should be app text(string), got %v",
|
||||
option.ACTION_SwipeToTapText, action.Params)
|
||||
case option.ACTION_SwipeToTapTexts:
|
||||
if texts, ok := action.Params.([]string); ok {
|
||||
return dExt.SwipeToTapTexts(texts, action.GetOptions()...)
|
||||
}
|
||||
if texts, err := builtin.ConvertToStringSlice(action.Params); err == nil {
|
||||
return dExt.SwipeToTapTexts(texts, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_SwipeToTapTexts, action.Params)
|
||||
case option.ACTION_AppTerminate:
|
||||
if bundleId, ok := action.Params.(string); ok {
|
||||
success, err := dExt.AppTerminate(bundleId)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to terminate app")
|
||||
}
|
||||
if !success {
|
||||
log.Warn().Str("bundleId", bundleId).Msg("app was not running")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
|
||||
case option.ACTION_Home:
|
||||
return dExt.Home()
|
||||
case option.ACTION_SecondaryClick:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
if len(params) != 2 {
|
||||
return fmt.Errorf("invalid tap location params: %v", params)
|
||||
}
|
||||
x, y := params[0], params[1]
|
||||
return dExt.SecondaryClick(x, y)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_SecondaryClick, action.Params)
|
||||
case option.ACTION_HoverBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
return dExt.HoverBySelector(selector, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_HoverBySelector, action.Params)
|
||||
case option.ACTION_TapBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
return dExt.TapBySelector(selector, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapBySelector, action.Params)
|
||||
case option.ACTION_SecondaryClickBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
return dExt.SecondaryClickBySelector(selector, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_SecondaryClickBySelector, action.Params)
|
||||
case option.ACTION_WebCloseTab:
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
paramInt64, _ := param.Int64()
|
||||
return dExt.IDriver.(*BrowserDriver).CloseTab(int(paramInt64))
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
return dExt.IDriver.(*BrowserDriver).CloseTab(int(param))
|
||||
} else {
|
||||
return dExt.IDriver.(*BrowserDriver).CloseTab(action.Params.(int))
|
||||
}
|
||||
// return fmt.Errorf("invalid %s params: %v", ACTION_WebCloseTab, action.Params)
|
||||
case option.ACTION_SetIme:
|
||||
if ime, ok := action.Params.(string); ok {
|
||||
err = dExt.SetIme(ime)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set ime")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case option.ACTION_GetSource:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
_, err = dExt.Source(option.WithProcessName(packageName))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set ime")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case option.ACTION_TapXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
// relative x,y of window size: [0.5, 0.5]
|
||||
if len(params) != 2 {
|
||||
return fmt.Errorf("invalid tap location params: %v", params)
|
||||
}
|
||||
x, y := params[0], params[1]
|
||||
return dExt.TapXY(x, y, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapXY, action.Params)
|
||||
case option.ACTION_TapAbsXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
// absolute coordinates x,y of window size: [100, 300]
|
||||
if len(params) != 2 {
|
||||
return fmt.Errorf("invalid tap location params: %v", params)
|
||||
}
|
||||
x, y := params[0], params[1]
|
||||
return dExt.TapAbsXY(x, y, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapAbsXY, action.Params)
|
||||
case option.ACTION_TapByOCR:
|
||||
if ocrText, ok := action.Params.(string); ok {
|
||||
return dExt.TapByOCR(ocrText, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapByOCR, action.Params)
|
||||
case option.ACTION_TapByCV:
|
||||
actionOptions := option.NewActionOptions(action.GetOptions()...)
|
||||
if len(actionOptions.ScreenShotWithUITypes) > 0 {
|
||||
return dExt.TapByCV(action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_TapByCV, action.Params)
|
||||
case option.ACTION_DoubleTapXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
// relative x,y of window size: [0.5, 0.5]
|
||||
if len(params) != 2 {
|
||||
return fmt.Errorf("invalid tap location params: %v", params)
|
||||
}
|
||||
x, y := params[0], params[1]
|
||||
return dExt.DoubleTap(x, y)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_DoubleTapXY, action.Params)
|
||||
case option.ACTION_Swipe:
|
||||
params := action.Params
|
||||
swipeAction := prepareSwipeAction(dExt, params, action.GetOptions()...)
|
||||
return swipeAction(dExt)
|
||||
case option.ACTION_Input:
|
||||
// input text on current active element
|
||||
// append \n to send text with enter
|
||||
// send \b\b\b to delete 3 chars
|
||||
param := fmt.Sprintf("%v", action.Params)
|
||||
return dExt.Input(param)
|
||||
case option.ACTION_Back:
|
||||
return dExt.Back()
|
||||
case option.ACTION_Sleep:
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
seconds, _ := param.Float64()
|
||||
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
|
||||
return nil
|
||||
} else if param, ok := action.Params.(float64); ok {
|
||||
time.Sleep(time.Duration(param*1000) * time.Millisecond)
|
||||
return nil
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
time.Sleep(time.Duration(param) * time.Second)
|
||||
return nil
|
||||
} else if sd, ok := action.Params.(SleepConfig); ok {
|
||||
sleepStrict(sd.StartTime, int64(sd.Seconds*1000))
|
||||
return nil
|
||||
} else if param, ok := action.Params.(string); ok {
|
||||
seconds, err := builtin.ConvertToFloat64(param)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid sleep params: %v(%T)", action.Params, action.Params)
|
||||
}
|
||||
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
|
||||
case option.ACTION_SleepMS:
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
milliseconds, _ := param.Int64()
|
||||
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
|
||||
return nil
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
time.Sleep(time.Duration(param) * time.Millisecond)
|
||||
return nil
|
||||
} else if sd, ok := action.Params.(SleepConfig); ok {
|
||||
sleepStrict(sd.StartTime, sd.Milliseconds)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid sleep ms params: %v(%T)", action.Params, action.Params)
|
||||
case option.ACTION_SleepRandom:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
sleepStrict(time.Now(), getSimulationDuration(params))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
|
||||
case option.ACTION_ScreenShot:
|
||||
// take screenshot
|
||||
log.Info().Msg("take screenshot for current screen")
|
||||
_, err := dExt.GetScreenResult(action.GetScreenShotOptions()...)
|
||||
return err
|
||||
case option.ACTION_ClosePopups:
|
||||
return dExt.ClosePopupsHandler()
|
||||
case option.ACTION_CallFunction:
|
||||
if funcDesc, ok := action.Params.(string); ok {
|
||||
return dExt.Call(funcDesc, action.Fn, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid function description: %v", action.Params)
|
||||
case option.ACTION_AIAction:
|
||||
if prompt, ok := action.Params.(string); ok {
|
||||
return dExt.AIAction(prompt, action.GetOptions()...)
|
||||
}
|
||||
return fmt.Errorf("invalid %s params: %v", option.ACTION_AIAction, action.Params)
|
||||
default:
|
||||
log.Warn().Str("action", string(action.Method)).Msg("action not implemented")
|
||||
return errors.Wrapf(code.InvalidCaseError,
|
||||
"UI action %v not implemented", action.Method)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
1330
uixt/mcp_server.go
1330
uixt/mcp_server.go
File diff suppressed because it is too large
Load Diff
72
uixt/mcp_server_test.go
Normal file
72
uixt/mcp_server_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewMCPServer(t *testing.T) {
|
||||
server := NewMCPServer()
|
||||
assert.NotNil(t, server)
|
||||
|
||||
// Check that tools are registered
|
||||
tools := server.ListTools()
|
||||
assert.Greater(t, len(tools), 0, "Should have at least one tool registered")
|
||||
|
||||
// Check specific tools exist
|
||||
expectedTools := []string{
|
||||
"list_available_devices",
|
||||
"select_device",
|
||||
"list_packages",
|
||||
"launch_app",
|
||||
"terminate_app",
|
||||
"get_screen_size",
|
||||
"press_button",
|
||||
"tap_xy",
|
||||
"swipe",
|
||||
"drag",
|
||||
"screenshot",
|
||||
"home",
|
||||
"back",
|
||||
"input",
|
||||
"sleep",
|
||||
}
|
||||
|
||||
registeredToolNames := make(map[string]bool)
|
||||
for _, tool := range tools {
|
||||
registeredToolNames[tool.Name] = true
|
||||
}
|
||||
|
||||
for _, expectedTool := range expectedTools {
|
||||
assert.True(t, registeredToolNames[expectedTool], "Tool %s should be registered", expectedTool)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolInterfaces(t *testing.T) {
|
||||
// Test that all tools implement the ActionTool interface correctly
|
||||
tools := []ActionTool{
|
||||
&ToolListAvailableDevices{},
|
||||
&ToolSelectDevice{},
|
||||
&ToolListPackages{},
|
||||
&ToolLaunchApp{},
|
||||
&ToolTerminateApp{},
|
||||
&ToolGetScreenSize{},
|
||||
&ToolPressButton{},
|
||||
&ToolTapXY{},
|
||||
&ToolSwipe{},
|
||||
&ToolDrag{},
|
||||
&ToolScreenShot{},
|
||||
&ToolHome{},
|
||||
&ToolBack{},
|
||||
&ToolInput{},
|
||||
&ToolSleep{},
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
assert.NotEmpty(t, tool.Name(), "Tool name should not be empty")
|
||||
assert.NotEmpty(t, tool.Description(), "Tool description should not be empty")
|
||||
assert.NotNil(t, tool.Options(), "Tool options should not be nil")
|
||||
assert.NotNil(t, tool.Implement(), "Tool implementation should not be nil")
|
||||
}
|
||||
}
|
||||
@@ -92,10 +92,125 @@ type PressButtonRequest struct {
|
||||
Button types.DeviceButton `json:"button" binding:"required" desc:"The button to press. Supported buttons: BACK (android only), HOME, VOLUME_UP, VOLUME_DOWN, ENTER."`
|
||||
}
|
||||
|
||||
// Additional requests for missing actions
|
||||
type WebLoginNoneUIRequest struct {
|
||||
TargetDeviceRequest
|
||||
PackageName string `json:"packageName" binding:"required" desc:"Package name for the app to login"`
|
||||
PhoneNumber string `json:"phoneNumber" binding:"required" desc:"Phone number for login"`
|
||||
Captcha string `json:"captcha" binding:"required" desc:"Captcha code"`
|
||||
Password string `json:"password" binding:"required" desc:"Password for login"`
|
||||
}
|
||||
|
||||
type SwipeToTapAppRequest struct {
|
||||
TargetDeviceRequest
|
||||
AppName string `json:"appName" binding:"required" desc:"App name to find and tap"`
|
||||
}
|
||||
|
||||
type SwipeToTapTextRequest struct {
|
||||
TargetDeviceRequest
|
||||
Text string `json:"text" binding:"required" desc:"Text to find and tap"`
|
||||
}
|
||||
|
||||
type SwipeToTapTextsRequest struct {
|
||||
TargetDeviceRequest
|
||||
Texts []string `json:"texts" binding:"required" desc:"List of texts to find and tap"`
|
||||
}
|
||||
|
||||
type SecondaryClickRequest struct {
|
||||
TargetDeviceRequest
|
||||
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||
}
|
||||
|
||||
type SelectorRequest struct {
|
||||
TargetDeviceRequest
|
||||
Selector string `json:"selector" binding:"required" desc:"CSS or XPath selector"`
|
||||
}
|
||||
|
||||
type WebCloseTabRequest struct {
|
||||
TargetDeviceRequest
|
||||
TabIndex int `json:"tabIndex" binding:"required" desc:"Index of the tab to close"`
|
||||
}
|
||||
|
||||
type SetImeRequest struct {
|
||||
TargetDeviceRequest
|
||||
Ime string `json:"ime" binding:"required" desc:"IME package name to set"`
|
||||
}
|
||||
|
||||
type GetSourceRequest struct {
|
||||
TargetDeviceRequest
|
||||
PackageName string `json:"packageName" binding:"required" desc:"Package name to get source from"`
|
||||
}
|
||||
|
||||
type TapAbsXYRequest struct {
|
||||
TargetDeviceRequest
|
||||
X float64 `json:"x" binding:"required" desc:"Absolute X coordinate in pixels"`
|
||||
Y float64 `json:"y" binding:"required" desc:"Absolute Y coordinate in pixels"`
|
||||
Duration float64 `json:"duration" desc:"Tap duration in seconds (optional)"`
|
||||
}
|
||||
|
||||
type TapByOCRRequest struct {
|
||||
TargetDeviceRequest
|
||||
Text string `json:"text" binding:"required" desc:"OCR text to find and tap"`
|
||||
}
|
||||
|
||||
type TapByCVRequest struct {
|
||||
TargetDeviceRequest
|
||||
ImagePath string `json:"imagePath" desc:"Path to reference image for CV recognition"`
|
||||
}
|
||||
|
||||
type DoubleTapXYRequest struct {
|
||||
TargetDeviceRequest
|
||||
X float64 `json:"x" binding:"required" desc:"X coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||
Y float64 `json:"y" binding:"required" desc:"Y coordinate (0.0~1.0 for percent, or absolute pixel value)"`
|
||||
}
|
||||
|
||||
type SwipeAdvancedRequest struct {
|
||||
TargetDeviceRequest
|
||||
FromX float64 `json:"fromX" binding:"required" desc:"Starting X coordinate"`
|
||||
FromY float64 `json:"fromY" binding:"required" desc:"Starting Y coordinate"`
|
||||
ToX float64 `json:"toX" binding:"required" desc:"Ending X coordinate"`
|
||||
ToY float64 `json:"toY" binding:"required" desc:"Ending Y coordinate"`
|
||||
Duration float64 `json:"duration" desc:"Swipe duration in seconds (optional)"`
|
||||
PressDuration float64 `json:"pressDuration" desc:"Press duration in seconds (optional)"`
|
||||
}
|
||||
|
||||
type SleepMSRequest struct {
|
||||
TargetDeviceRequest
|
||||
Milliseconds int64 `json:"milliseconds" binding:"required" desc:"Sleep duration in milliseconds"`
|
||||
}
|
||||
|
||||
type SleepRandomRequest struct {
|
||||
TargetDeviceRequest
|
||||
Params []float64 `json:"params" binding:"required" desc:"Random sleep parameters [min, max] or [min1, max1, weight1, ...]"`
|
||||
}
|
||||
|
||||
type CallFunctionRequest struct {
|
||||
TargetDeviceRequest
|
||||
Description string `json:"description" binding:"required" desc:"Function description"`
|
||||
}
|
||||
|
||||
type AIActionRequest struct {
|
||||
TargetDeviceRequest
|
||||
Prompt string `json:"prompt" binding:"required" desc:"AI action prompt"`
|
||||
}
|
||||
|
||||
// NewMCPOptions generates mcp.NewTool parameters from a struct type.
|
||||
// It automatically generates mcp.NewTool parameters based on the struct fields and their desc tags.
|
||||
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")
|
||||
@@ -125,6 +240,23 @@ func NewMCPOptions(t interface{}) (options []mcp.ToolOption) {
|
||||
} 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")
|
||||
}
|
||||
|
||||
769
uixt/sdk.go
769
uixt/sdk.go
@@ -2,8 +2,10 @@ package uixt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
@@ -78,28 +80,755 @@ func (c *MCPClient4XTDriver) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertActionToCallToolRequest(action MobileAction) (mcp.CallToolRequest, error) {
|
||||
// req := mcp.CallToolRequest{
|
||||
// Params: struct {
|
||||
// Name string `json:"name"`
|
||||
// Arguments map[string]any `json:"arguments,omitempty"`
|
||||
// Meta *struct {
|
||||
// ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
// } `json:"_meta,omitempty"`
|
||||
// }{
|
||||
// Name: action.Method,
|
||||
// Arguments: action.Params,
|
||||
// },
|
||||
// }
|
||||
return mcp.CallToolRequest{}, nil
|
||||
}
|
||||
|
||||
func (dExt *XTDriver) ExecuteAction(action MobileAction) (err error) {
|
||||
// convert action to call tool request
|
||||
// Convert action to MCP tool call
|
||||
req, err := convertActionToCallToolRequest(action)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to convert action to MCP tool call: %w", err)
|
||||
}
|
||||
|
||||
// Execute via MCP tool
|
||||
result, err := dExt.client.CallTool(context.Background(), req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MCP tool call failed: %w", err)
|
||||
}
|
||||
|
||||
// Check if the tool execution had business logic errors
|
||||
if result.IsError {
|
||||
if len(result.Content) > 0 {
|
||||
return fmt.Errorf("tool execution failed: %s", result.Content[0])
|
||||
}
|
||||
return fmt.Errorf("tool execution failed")
|
||||
}
|
||||
|
||||
log.Debug().Str("method", string(action.Method)).Msg("executed action via MCP tool")
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertActionToCallToolRequest(action MobileAction) (mcp.CallToolRequest, error) {
|
||||
var arguments map[string]interface{}
|
||||
|
||||
switch action.Method {
|
||||
case option.ACTION_WebLoginNoneUI:
|
||||
if params, ok := action.Params.([]interface{}); ok && len(params) == 4 {
|
||||
arguments = map[string]interface{}{
|
||||
"packageName": params[0].(string),
|
||||
"phoneNumber": params[1].(string),
|
||||
"captcha": params[2].(string),
|
||||
"password": params[3].(string),
|
||||
}
|
||||
} else if params, ok := action.Params.([]string); ok && len(params) == 4 {
|
||||
arguments = map[string]interface{}{
|
||||
"packageName": params[0],
|
||||
"phoneNumber": params[1],
|
||||
"captcha": params[2],
|
||||
"password": params[3],
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid web login params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "web_login_none_ui",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_AppInstall:
|
||||
if app, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"appUrl": app,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid app install params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "app_install",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_AppUninstall:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"packageName": packageName,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid app uninstall params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "app_uninstall",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_AppClear:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"packageName": packageName,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid app clear params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "app_clear",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_AppLaunch:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"packageName": packageName,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid app launch params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "launch_app",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_SwipeToTapApp:
|
||||
if appName, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"appName": appName,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap app params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "swipe_to_tap_app",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_SwipeToTapText:
|
||||
if text, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"text": text,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap text params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "swipe_to_tap_text",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_SwipeToTapTexts:
|
||||
var texts []string
|
||||
if textsSlice, ok := action.Params.([]string); ok {
|
||||
texts = textsSlice
|
||||
} else if textsInterface, err := builtin.ConvertToStringSlice(action.Params); err == nil {
|
||||
texts = textsInterface
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe to tap texts params: %v", action.Params)
|
||||
}
|
||||
arguments = map[string]interface{}{
|
||||
"texts": texts,
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "swipe_to_tap_texts",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_AppTerminate:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"packageName": packageName,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid app terminate params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "terminate_app",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_Home:
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "home",
|
||||
Arguments: map[string]interface{}{},
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_SecondaryClick:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil && len(params) == 2 {
|
||||
arguments = map[string]interface{}{
|
||||
"x": params[0],
|
||||
"y": params[1],
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid secondary click params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "secondary_click",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_HoverBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"selector": selector,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid hover by selector params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "hover_by_selector",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_TapBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"selector": selector,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by selector params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "tap_by_selector",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_SecondaryClickBySelector:
|
||||
if selector, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"selector": selector,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid secondary click by selector params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "secondary_click_by_selector",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_WebCloseTab:
|
||||
var tabIndex int
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
paramInt64, _ := param.Int64()
|
||||
tabIndex = int(paramInt64)
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
tabIndex = int(param)
|
||||
} else if param, ok := action.Params.(int); ok {
|
||||
tabIndex = param
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid web close tab params: %v", action.Params)
|
||||
}
|
||||
arguments = map[string]interface{}{
|
||||
"tabIndex": tabIndex,
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "web_close_tab",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_SetIme:
|
||||
if ime, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"ime": ime,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid set ime params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "set_ime",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_GetSource:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"packageName": packageName,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid get source params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "get_source",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_TapXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil && len(params) == 2 {
|
||||
x, y := params[0], params[1]
|
||||
arguments = map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
}
|
||||
// Add duration if available from action options
|
||||
if actionOptions := action.GetOptions(); len(actionOptions) > 0 {
|
||||
for _, opt := range actionOptions {
|
||||
if opt != nil {
|
||||
// Add options like duration
|
||||
if duration := action.ActionOptions.Duration; duration > 0 {
|
||||
arguments["duration"] = duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "tap_xy",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_TapAbsXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil && len(params) == 2 {
|
||||
x, y := params[0], params[1]
|
||||
arguments = map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
}
|
||||
// Add duration if available
|
||||
if duration := action.ActionOptions.Duration; duration > 0 {
|
||||
arguments["duration"] = duration
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap abs params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "tap_abs_xy",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_TapByOCR:
|
||||
if text, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"text": text,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid tap by OCR params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "tap_by_ocr",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_TapByCV:
|
||||
// For TapByCV, the original action might not have params but relies on options
|
||||
arguments = map[string]interface{}{
|
||||
"imagePath": "", // Will be handled by the tool based on UI types
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "tap_by_cv",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_DoubleTapXY:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil && len(params) == 2 {
|
||||
x, y := params[0], params[1]
|
||||
arguments = map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid double tap params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "double_tap_xy",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_Swipe:
|
||||
// Handle different types of swipe params
|
||||
switch params := action.Params.(type) {
|
||||
case string:
|
||||
// Direction swipe like "up", "down", "left", "right"
|
||||
arguments = map[string]interface{}{
|
||||
"direction": params,
|
||||
}
|
||||
// Add duration and press duration from options
|
||||
if duration := action.ActionOptions.Duration; duration > 0 {
|
||||
arguments["duration"] = duration
|
||||
}
|
||||
if pressDuration := action.ActionOptions.PressDuration; pressDuration > 0 {
|
||||
arguments["pressDuration"] = pressDuration
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "swipe",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
// Advanced swipe with coordinates
|
||||
if paramSlice, err := builtin.ConvertToFloat64Slice(params); err == nil && len(paramSlice) == 4 {
|
||||
arguments = map[string]interface{}{
|
||||
"fromX": paramSlice[0],
|
||||
"fromY": paramSlice[1],
|
||||
"toX": paramSlice[2],
|
||||
"toY": paramSlice[3],
|
||||
}
|
||||
// Add duration and press duration from options
|
||||
if duration := action.ActionOptions.Duration; duration > 0 {
|
||||
arguments["duration"] = duration
|
||||
}
|
||||
if pressDuration := action.ActionOptions.PressDuration; pressDuration > 0 {
|
||||
arguments["pressDuration"] = pressDuration
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "swipe_advanced",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid swipe params: %v", action.Params)
|
||||
|
||||
case option.ACTION_Input:
|
||||
text := fmt.Sprintf("%v", action.Params)
|
||||
arguments = map[string]interface{}{
|
||||
"text": text,
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "input",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_Back:
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "back",
|
||||
Arguments: map[string]interface{}{},
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_Sleep:
|
||||
arguments = map[string]interface{}{
|
||||
"seconds": action.Params,
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "sleep",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_SleepMS:
|
||||
var milliseconds int64
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
milliseconds, _ = param.Int64()
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
milliseconds = param
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep ms params: %v", action.Params)
|
||||
}
|
||||
arguments = map[string]interface{}{
|
||||
"milliseconds": milliseconds,
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "sleep_ms",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_SleepRandom:
|
||||
if params, err := builtin.ConvertToFloat64Slice(action.Params); err == nil {
|
||||
arguments = map[string]interface{}{
|
||||
"params": params,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep random params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "sleep_random",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_ScreenShot:
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "screenshot",
|
||||
Arguments: map[string]interface{}{},
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_ClosePopups:
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "close_popups",
|
||||
Arguments: map[string]interface{}{},
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_CallFunction:
|
||||
if description, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"description": description,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid call function params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "call_function",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
case option.ACTION_AIAction:
|
||||
if prompt, ok := action.Params.(string); ok {
|
||||
arguments = map[string]interface{}{
|
||||
"prompt": prompt,
|
||||
}
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid AI action params: %v", action.Params)
|
||||
}
|
||||
return mcp.CallToolRequest{
|
||||
Params: struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Meta *struct {
|
||||
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
|
||||
} `json:"_meta,omitempty"`
|
||||
}{
|
||||
Name: "ai_action",
|
||||
Arguments: arguments,
|
||||
},
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("unsupported action method: %s", action.Method)
|
||||
}
|
||||
_, err = dExt.client.CallTool(context.Background(), req)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user