Merge 'mcp_sim_dev' into 'master'

Mcp sim dev

See merge request: !141
This commit is contained in:
李隆
2025-07-30 08:25:29 +00:00
11 changed files with 793 additions and 93 deletions

View File

@@ -2,10 +2,12 @@ package uitest
import (
"fmt"
"os"
"strconv"
"strings"
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/types"
@@ -256,30 +258,6 @@ func TestSwipeWithDirection(t *testing.T) {
minDistance: 100.0,
maxDistance: 500.0,
},
{
name: "随机距离下滑",
direction: "down",
startX: 0.5,
startY: 0.5,
minDistance: 150.0,
maxDistance: 350.0, // 范围内随机
},
{
name: "固定距离左滑",
direction: "left",
startX: 0.5,
startY: 0.5,
minDistance: 300.0,
maxDistance: 300.0,
},
{
name: "随机距离右滑",
direction: "right",
startX: 0.6,
startY: 0.5,
minDistance: 100.0,
maxDistance: 250.0,
},
}
for _, tc := range testCases {
@@ -567,41 +545,9 @@ func TestSIMInput(t *testing.T) {
name string
text string
}{
{
name: "英文短文本",
text: "Hello",
},
{
name: "英文长文本",
text: "Hello World! This is a test message.",
},
{
name: "日文文本",
text: "英語の長い文字",
},
{
name: "混合文本",
text: "Hello你好123",
},
{
name: "特殊字符",
text: "!@#$%^&*()",
},
{
name: "数字文本",
text: "1234567890",
},
{
name: "空文本",
text: "",
},
{
name: "单个字符",
text: "A",
},
{
name: "长文本",
text: "This is a very long text to test the performance of SIMInput function. 这是一个很长的文本用来测试SIMInput函数的性能。1234567890!@#$%^&*()英語の長い文",
text: "This is a very long text to test the performance of SIMInput function. 这是一个很长的文本用来测试SIMInput函数的性能。1234567890!@#$%^&*()英語の長い文",
},
}
@@ -617,3 +563,42 @@ func TestSIMInput(t *testing.T) {
})
}
}
// TestStepMultipleSIMActions tests multiple SIM actions in one test case
func TestStepMultipleSIMActions(t *testing.T) {
// 创建包含多个SIM操作的测试用例
testCase := &hrp.TestCase{
Config: hrp.NewConfig("多个SIM操作组合测试").SetAndroid(option.WithUIA2(true), option.WithSerialNumber("")),
TestSteps: []hrp.IStep{
hrp.NewStep("组合SIM操作测试").
Android().
SIMClickAtPoint(0.5, 0.5). // 点击屏幕中心
Sleep(1). // 等待1秒
SIMSwipeWithDirection("up", 0.5, 0.7, 200.0, 400.0). // 向上滑动
Sleep(0.5). // 等待0.5秒
SIMSwipeInArea("up", 0.2, 0.2, 0.6, 0.6, 350.0, 500.0). // 在区域内向下滑动
Sleep(0.5). // 等待0.5秒
SIMSwipeFromPointToPoint(0.1, 0.5, 0.9, 0.5). // 从左到右滑动
Sleep(0.5). // 等待0.5秒
SIMInput("测试组合操作 Test Combination 123"), // 仿真输入
},
}
// 运行测试用例
err := testCase.Dump2JSON("TestStepMultipleSIMActions.json")
if err != nil {
t.Fatalf("Failed to dump test case: %v", err)
}
defer func() {
// 清理生成的文件
_ = os.Remove("TestStepMultipleSIMActions.json")
}()
// 执行测试用例
err = hrp.NewRunner(t).Run(testCase)
if err != nil {
t.Errorf("Test case failed: %v", err)
}
t.Logf("Successfully executed multiple SIM actions test")
}

View File

@@ -61,7 +61,7 @@ type ClickConfig struct {
var DefaultClickConfig = ClickConfig{
MinDuration: 40,
MaxDuration: 90,
MinPoints: 3,
MinPoints: 4, // 增加最小点数从3到4确保至少有2个MOVE事件
MaxPoints: 6,
MaxDeviation: 2.0,
NoiseLevel: 0.5,

View File

@@ -1 +1 @@
v5.0.0-250728
v5.0.0-250730

View File

@@ -290,6 +290,100 @@ func (s *StepMobile) SwipeRight(opts ...option.ActionOption) *StepMobile {
return s
}
// SIMSwipeWithDirection performs simulated swipe in specified direction with random distance
func (s *StepMobile) SIMSwipeWithDirection(direction string, fromX, fromY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) *StepMobile {
// Create params map for SIMSwipeWithDirection
params := map[string]interface{}{
"direction": direction,
"from_x": fromX,
"from_y": fromY,
"sim_min_distance": simMinDistance,
"sim_max_distance": simMaxDistance,
}
action := option.MobileAction{
Method: option.ACTION_SIMSwipeDirection,
Params: params,
Options: option.NewActionOptions(opts...),
}
s.obj().Actions = append(s.obj().Actions, action)
return s
}
// SIMSwipeInArea performs simulated swipe in specified area with direction and random distance
func (s *StepMobile) SIMSwipeInArea(direction string, simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) *StepMobile {
// Create params map for SIMSwipeInArea
params := map[string]interface{}{
"direction": direction,
"sim_area_start_x": simAreaStartX,
"sim_area_start_y": simAreaStartY,
"sim_area_end_x": simAreaEndX,
"sim_area_end_y": simAreaEndY,
"sim_min_distance": simMinDistance,
"sim_max_distance": simMaxDistance,
}
action := option.MobileAction{
Method: option.ACTION_SIMSwipeInArea,
Params: params,
Options: option.NewActionOptions(opts...),
}
s.obj().Actions = append(s.obj().Actions, action)
return s
}
// SIMSwipeFromPointToPoint performs simulated swipe from point to point
func (s *StepMobile) SIMSwipeFromPointToPoint(fromX, fromY, toX, toY float64, opts ...option.ActionOption) *StepMobile {
// Create params map for SIMSwipeFromPointToPoint
params := map[string]interface{}{
"from_x": fromX,
"from_y": fromY,
"to_x": toX,
"to_y": toY,
}
action := option.MobileAction{
Method: option.ACTION_SIMSwipeFromPointToPoint,
Params: params,
Options: option.NewActionOptions(opts...),
}
s.obj().Actions = append(s.obj().Actions, action)
return s
}
// SIMClickAtPoint performs simulated click at specified point
func (s *StepMobile) SIMClickAtPoint(x, y float64, opts ...option.ActionOption) *StepMobile {
// Create params map for SIMClickAtPoint
params := map[string]interface{}{
"x": x,
"y": y,
}
action := option.MobileAction{
Method: option.ACTION_SIMClickAtPoint,
Params: params,
Options: option.NewActionOptions(opts...),
}
s.obj().Actions = append(s.obj().Actions, action)
return s
}
// SIMInput performs simulated text input with intelligent segmentation
func (s *StepMobile) SIMInput(text string, opts ...option.ActionOption) *StepMobile {
action := option.MobileAction{
Method: option.ACTION_SIMInput,
Params: text,
Options: option.NewActionOptions(opts...),
}
s.obj().Actions = append(s.obj().Actions, action)
return s
}
func (s *StepMobile) SwipeToTapApp(appName string, opts ...option.ActionOption) *StepMobile {
action := option.MobileAction{
Method: option.ACTION_SwipeToTapApp,

View File

@@ -533,7 +533,6 @@ func (ud *UIA2Driver) TouchByEvents(events []types.TouchEvent, opts ...option.Ac
log.Warn().Int("action", event.Action).Msg("Unknown action type, skipping")
continue
}
actions = append(actions, actionMap)
}
@@ -556,10 +555,10 @@ func (ud *UIA2Driver) TouchByEvents(events []types.TouchEvent, opts ...option.Ac
// SwipeWithDirection 向指定方向滑动任意距离
// direction: 滑动方向 ("up", "down", "left", "right")
// startX, startY: 起始坐标
// minDistance, maxDistance: 距离范围,如果相等则为固定距离,否则为随机距离
func (ud *UIA2Driver) SIMSwipeWithDirection(direction string, startX, startY, minDistance, maxDistance float64, opts ...option.ActionOption) error {
absStartX, absStartY, err := convertToAbsolutePoint(ud, startX, startY)
// fromX, fromY: 起始坐标
// simMinDistance, simMaxDistance: 距离范围,如果相等则为固定距离,否则为随机距离
func (ud *UIA2Driver) SIMSwipeWithDirection(direction string, fromX, fromY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) error {
absStartX, absStartY, err := convertToAbsolutePoint(ud, fromX, fromY)
if err != nil {
return err
}
@@ -569,7 +568,7 @@ func (ud *UIA2Driver) SIMSwipeWithDirection(direction string, startX, startY, mi
log.Info().Str("direction", direction).
Float64("startX", absStartX).Float64("startY", absStartY).
Float64("minDistance", minDistance).Float64("maxDistance", maxDistance).
Float64("minDistance", simMinDistance).Float64("maxDistance", simMaxDistance).
Str("deviceModel", deviceModel).
Int("deviceID", deviceParams.DeviceID).
Float64("pressure", deviceParams.Pressure).
@@ -596,7 +595,7 @@ func (ud *UIA2Driver) SIMSwipeWithDirection(direction string, startX, startY, mi
// 使用滑动仿真算法生成触摸事件序列
events, err := simulator.GenerateSlideWithRandomDistance(
absStartX, absStartY, slideDirection, minDistance, maxDistance,
absStartX, absStartY, slideDirection, simMinDistance, simMaxDistance,
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
if err != nil {
return fmt.Errorf("generate slide events failed: %v", err)
@@ -608,15 +607,15 @@ func (ud *UIA2Driver) SIMSwipeWithDirection(direction string, startX, startY, mi
// SwipeInArea 在指定区域内向指定方向滑动任意距离
// direction: 滑动方向 ("up", "down", "left", "right")
// areaStartX, areaStartY, areaEndX, areaEndY: 区域范围(相对坐标)
// minDistance, maxDistance: 距离范围,如果相等则为固定距离,否则为随机距离
func (ud *UIA2Driver) SIMSwipeInArea(direction string, areaStartX, areaStartY, areaEndX, areaEndY, minDistance, maxDistance float64, opts ...option.ActionOption) error {
// simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY: 区域范围(相对坐标)
// simMinDistance, simMaxDistance: 距离范围,如果相等则为固定距离,否则为随机距离
func (ud *UIA2Driver) SIMSwipeInArea(direction string, simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) error {
// 转换区域坐标为绝对坐标
absAreaStartX, absAreaStartY, err := convertToAbsolutePoint(ud, areaStartX, areaStartY)
absAreaStartX, absAreaStartY, err := convertToAbsolutePoint(ud, simAreaStartX, simAreaStartY)
if err != nil {
return err
}
absAreaEndX, absAreaEndY, err := convertToAbsolutePoint(ud, areaEndX, areaEndY)
absAreaEndX, absAreaEndY, err := convertToAbsolutePoint(ud, simAreaEndX, simAreaEndY)
if err != nil {
return err
}
@@ -636,7 +635,7 @@ func (ud *UIA2Driver) SIMSwipeInArea(direction string, areaStartX, areaStartY, a
log.Info().Str("direction", direction).
Float64("areaStartX", absAreaStartX).Float64("areaStartY", absAreaStartY).
Float64("areaEndX", absAreaEndX).Float64("areaEndY", absAreaEndY).
Float64("minDistance", minDistance).Float64("maxDistance", maxDistance).
Float64("minDistance", simMinDistance).Float64("maxDistance", simMaxDistance).
Str("deviceModel", deviceModel).
Int("deviceID", deviceParams.DeviceID).
Float64("pressure", deviceParams.Pressure).
@@ -664,7 +663,7 @@ func (ud *UIA2Driver) SIMSwipeInArea(direction string, areaStartX, areaStartY, a
// 使用滑动仿真算法生成区域内滑动的触摸事件序列
events, err := simulator.GenerateSlideInArea(
absAreaStartX, absAreaStartY, absAreaEndX, absAreaEndY,
slideDirection, minDistance, maxDistance,
slideDirection, simMinDistance, simMaxDistance,
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
if err != nil {
return fmt.Errorf("generate slide in area events failed: %v", err)
@@ -675,15 +674,15 @@ func (ud *UIA2Driver) SIMSwipeInArea(direction string, areaStartX, areaStartY, a
}
// SwipeFromPointToPoint 指定起始点和结束点进行滑动
// startX, startY: 起始坐标(相对坐标)
// endX, endY: 结束坐标(相对坐标)
func (ud *UIA2Driver) SIMSwipeFromPointToPoint(startX, startY, endX, endY float64, opts ...option.ActionOption) error {
// fromX, fromY: 起始坐标(相对坐标)
// toX, toY: 结束坐标(相对坐标)
func (ud *UIA2Driver) SIMSwipeFromPointToPoint(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error {
// 转换起始点和结束点为绝对坐标
absStartX, absStartY, err := convertToAbsolutePoint(ud, startX, startY)
absStartX, absStartY, err := convertToAbsolutePoint(ud, fromX, fromY)
if err != nil {
return err
}
absEndX, absEndY, err := convertToAbsolutePoint(ud, endX, endY)
absEndX, absEndY, err := convertToAbsolutePoint(ud, toX, toY)
if err != nil {
return err
}

View File

@@ -15,6 +15,9 @@ var (
_ IDriver = (*WDADriver)(nil)
_ IDriver = (*HDCDriver)(nil)
_ IDriver = (*BrowserDriver)(nil)
// Ensure drivers implement SIMSupport interface
_ SIMSupport = (*UIA2Driver)(nil)
)
// current implemeted driver: ADBDriver, UIA2Driver, WDADriver, HDCDriver
@@ -90,3 +93,13 @@ type IDriver interface {
// clipboard operations
GetPasteboard() (string, error)
}
// SIMSupport interface defines simulated interaction methods
// Any driver that supports simulated touch and input should implement this interface
type SIMSupport interface {
SIMClickAtPoint(x, y float64, opts ...option.ActionOption) error
SIMSwipeWithDirection(direction string, fromX, fromY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) error
SIMSwipeInArea(direction string, simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY, simMinDistance, simMaxDistance float64, opts ...option.ActionOption) error
SIMSwipeFromPointToPoint(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error
SIMInput(text string, opts ...option.ActionOption) error
}

View File

@@ -87,23 +87,28 @@ func (s *MCPServer4XTDriver) registerTools() {
s.registerTool(&ToolSelectDevice{}) // SelectDevice
// Touch Tools
s.registerTool(&ToolTapXY{}) // tap xy
s.registerTool(&ToolTapAbsXY{}) // tap abs xy
s.registerTool(&ToolTapByOCR{}) // tap by OCR
s.registerTool(&ToolTapByCV{}) // tap by CV
s.registerTool(&ToolDoubleTapXY{}) // double tap xy
s.registerTool(&ToolTapXY{}) // tap xy
s.registerTool(&ToolTapAbsXY{}) // tap abs xy
s.registerTool(&ToolTapByOCR{}) // tap by OCR
s.registerTool(&ToolTapByCV{}) // tap by CV
s.registerTool(&ToolDoubleTapXY{}) // double tap xy
s.registerTool(&ToolSIMClickAtPoint{}) // simulated click at point
// Swipe Tools
s.registerTool(&ToolSwipe{}) // generic swipe, auto-detect direction or coordinate
s.registerTool(&ToolSwipeDirection{}) // swipe direction, up/down/left/right
s.registerTool(&ToolSwipeCoordinate{}) // swipe coordinate, [fromX, fromY, toX, toY]
s.registerTool(&ToolSwipe{}) // generic swipe, auto-detect direction or coordinate
s.registerTool(&ToolSwipeDirection{}) // swipe direction, up/down/left/right
s.registerTool(&ToolSwipeCoordinate{}) // swipe coordinate, [fromX, fromY, toX, toY]
s.registerTool(&ToolSIMSwipeDirection{}) // simulated swipe direction with random distance
s.registerTool(&ToolSIMSwipeInArea{}) // simulated swipe in area with direction and distance
s.registerTool(&ToolSIMSwipeFromPointToPoint{}) // simulated swipe from point to point
s.registerTool(&ToolSwipeToTapApp{})
s.registerTool(&ToolSwipeToTapText{})
s.registerTool(&ToolSwipeToTapTexts{})
s.registerTool(&ToolDrag{})
// Input Tools
s.registerTool(&ToolInput{})
s.registerTool(&ToolInput{}) // regular input
s.registerTool(&ToolSIMInput{}) // simulated input with intelligent segmentation
s.registerTool(&ToolBackspace{})
s.registerTool(&ToolSetIme{})

View File

@@ -6,6 +6,7 @@ import (
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/uixt/option"
)
@@ -192,3 +193,83 @@ func (t *ToolBackspace) ConvertActionToCallToolRequest(action option.MobileActio
}
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
}
// ToolSIMInput implements the sim_input tool call.
type ToolSIMInput struct {
// Return data fields - these define the structure of data returned by this tool
Text string `json:"text" desc:"Text that was input with simulation"`
Segments int `json:"segments" desc:"Number of segments the text was split into"`
}
func (t *ToolSIMInput) Name() option.ActionName {
return option.ACTION_SIMInput
}
func (t *ToolSIMInput) Description() string {
return "Input text with intelligent segmentation and human-like typing patterns"
}
func (t *ToolSIMInput) Options() []mcp.ToolOption {
unifiedReq := &option.ActionOptions{}
return unifiedReq.GetMCPOptions(option.ACTION_SIMInput)
}
func (t *ToolSIMInput) Implement() server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.GetArguments()
driverExt, err := setupXTDriver(ctx, arguments)
if err != nil {
return nil, fmt.Errorf("setup driver failed: %w", err)
}
unifiedReq, err := parseActionOptions(arguments)
if err != nil {
return nil, err
}
if unifiedReq.Text == "" {
return nil, fmt.Errorf("text is required")
}
text := unifiedReq.Text
log.Info().
Str("text", text).
Int("textLength", len(text)).
Msg("performing simulated input")
opts := unifiedReq.Options()
// Call the underlying SIMInput method (check if driver supports SIM)
if simDriver, ok := driverExt.IDriver.(SIMSupport); ok {
err = simDriver.SIMInput(text, opts...)
if err != nil {
return NewMCPErrorResponse(fmt.Sprintf("Simulated input failed: %s", err.Error())), err
}
} else {
return NewMCPErrorResponse("SIMInput is not supported by the current driver"), fmt.Errorf("driver does not implement SIMSupport interface")
}
// Estimate segments count (this is approximate since the actual segmentation happens in the driver)
estimatedSegments := len([]rune(text))/2 + 1
if estimatedSegments < 1 {
estimatedSegments = 1
}
message := fmt.Sprintf("Successfully performed simulated input: %s", text)
returnData := ToolSIMInput{
Text: text,
Segments: estimatedSegments,
}
return NewMCPSuccessResponse(message, &returnData), nil
}
}
func (t *ToolSIMInput) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
text := fmt.Sprintf("%v", action.Params)
arguments := map[string]any{
"text": text,
}
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
}

View File

@@ -547,3 +547,412 @@ func (t *ToolDrag) ConvertActionToCallToolRequest(action option.MobileAction) (m
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid drag parameters: %v", action.Params)
}
// ToolSIMSwipeDirection implements the sim_swipe_direction tool call.
type ToolSIMSwipeDirection struct {
// Return data fields - these define the structure of data returned by this tool
Direction string `json:"direction" desc:"Direction that was swiped (up/down/left/right)"`
StartX float64 `json:"startX" desc:"Starting X coordinate of the simulated swipe"`
StartY float64 `json:"startY" desc:"Starting Y coordinate of the simulated swipe"`
MinDistance float64 `json:"minDistance" desc:"Minimum distance of the simulated swipe"`
MaxDistance float64 `json:"maxDistance" desc:"Maximum distance of the simulated swipe"`
ActualDistance float64 `json:"actualDistance" desc:"Actual distance of the simulated swipe"`
}
func (t *ToolSIMSwipeDirection) Name() option.ActionName {
return option.ACTION_SIMSwipeDirection
}
func (t *ToolSIMSwipeDirection) Description() string {
return "Perform simulated swipe in specified direction with random distance and human-like touch patterns"
}
func (t *ToolSIMSwipeDirection) Options() []mcp.ToolOption {
unifiedReq := &option.ActionOptions{}
return unifiedReq.GetMCPOptions(option.ACTION_SIMSwipeDirection)
}
func (t *ToolSIMSwipeDirection) Implement() server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.GetArguments()
driverExt, err := setupXTDriver(ctx, arguments)
if err != nil {
return nil, fmt.Errorf("setup driver failed: %w", err)
}
unifiedReq, err := parseActionOptions(arguments)
if err != nil {
return nil, err
}
// Validate required parameters
if unifiedReq.Direction == nil {
return nil, fmt.Errorf("direction parameter is required")
}
direction, ok := unifiedReq.Direction.(string)
if !ok {
return nil, fmt.Errorf("direction must be a string")
}
// Validate direction
validDirections := []string{"up", "down", "left", "right"}
if !slices.Contains(validDirections, direction) {
return nil, fmt.Errorf("invalid swipe direction: %s, expected one of: %v",
direction, validDirections)
}
// Default values if not provided - use fromX/fromY instead of startX/startY
fromX := unifiedReq.FromX
fromY := unifiedReq.FromY
simMinDistance := unifiedReq.SIMMinDistance
simMaxDistance := unifiedReq.SIMMaxDistance
if fromX == 0 {
fromX = 0.5 // default to center
}
if fromY == 0 {
fromY = 0.5 // default to center
}
if simMinDistance == 0 {
simMinDistance = 100 // default minimum distance
}
if simMaxDistance == 0 {
simMaxDistance = 300 // default maximum distance
}
log.Info().
Str("direction", direction).
Float64("startX", fromX).
Float64("startY", fromY).
Float64("minDistance", simMinDistance).
Float64("maxDistance", simMaxDistance).
Msg("performing simulated swipe with direction")
// Build all options from request arguments
opts := unifiedReq.Options()
// Call the underlying SIMSwipeWithDirection method (check if driver supports SIM)
if simDriver, ok := driverExt.IDriver.(SIMSupport); ok {
err = simDriver.SIMSwipeWithDirection(direction, fromX, fromY, simMinDistance, simMaxDistance, opts...)
if err != nil {
return NewMCPErrorResponse(fmt.Sprintf("Simulated swipe failed: %s", err.Error())), err
}
} else {
return NewMCPErrorResponse("SIMSwipeWithDirection is not supported by the current driver"), fmt.Errorf("driver does not implement SIMSupport interface")
}
// Calculate actual distance for response (approximate)
actualDistance := simMinDistance
if simMaxDistance > simMinDistance {
actualDistance = simMinDistance + (simMaxDistance-simMinDistance)*0.5 // approximate middle value
}
message := fmt.Sprintf("Successfully performed simulated swipe %s from (%.2f, %.2f) with distance %.2f",
direction, fromX, fromY, actualDistance)
returnData := ToolSIMSwipeDirection{
Direction: direction,
StartX: fromX,
StartY: fromY,
MinDistance: simMinDistance,
MaxDistance: simMaxDistance,
ActualDistance: actualDistance,
}
return NewMCPSuccessResponse(message, &returnData), nil
}
}
func (t *ToolSIMSwipeDirection) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
// Handle params as map[string]interface{}
if paramsMap, ok := action.Params.(map[string]interface{}); ok {
arguments := map[string]any{}
// Extract direction
if direction, exists := paramsMap["direction"]; exists {
arguments["direction"] = direction
}
// Extract coordinates and distances - use new field names directly
if fromX, exists := paramsMap["from_x"]; exists {
arguments["from_x"] = fromX
}
if fromY, exists := paramsMap["from_y"]; exists {
arguments["from_y"] = fromY
}
if minDistance, exists := paramsMap["sim_min_distance"]; exists {
arguments["sim_min_distance"] = minDistance
}
if maxDistance, exists := paramsMap["sim_max_distance"]; exists {
arguments["sim_max_distance"] = maxDistance
}
// 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 BuildMCPCallToolRequest(t.Name(), arguments, action), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid SIM swipe direction params: %v", action.Params)
}
// ToolSIMSwipeInArea implements the sim_swipe_in_area tool call.
type ToolSIMSwipeInArea struct {
// Return data fields - these define the structure of data returned by this tool
Direction string `json:"direction" desc:"Direction that was swiped (up/down/left/right)"`
AreaStartX float64 `json:"areaStartX" desc:"Area starting X coordinate"`
AreaStartY float64 `json:"areaStartY" desc:"Area starting Y coordinate"`
AreaEndX float64 `json:"areaEndX" desc:"Area ending X coordinate"`
AreaEndY float64 `json:"areaEndY" desc:"Area ending Y coordinate"`
MinDistance float64 `json:"minDistance" desc:"Minimum distance of the simulated swipe"`
MaxDistance float64 `json:"maxDistance" desc:"Maximum distance of the simulated swipe"`
}
func (t *ToolSIMSwipeInArea) Name() option.ActionName {
return option.ACTION_SIMSwipeInArea
}
func (t *ToolSIMSwipeInArea) Description() string {
return "Perform simulated swipe in specified area with direction and random distance"
}
func (t *ToolSIMSwipeInArea) Options() []mcp.ToolOption {
unifiedReq := &option.ActionOptions{}
return unifiedReq.GetMCPOptions(option.ACTION_SIMSwipeInArea)
}
func (t *ToolSIMSwipeInArea) Implement() server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.GetArguments()
driverExt, err := setupXTDriver(ctx, arguments)
if err != nil {
return nil, fmt.Errorf("setup driver failed: %w", err)
}
unifiedReq, err := parseActionOptions(arguments)
if err != nil {
return nil, err
}
// Validate required parameters
if unifiedReq.Direction == nil {
return nil, fmt.Errorf("direction parameter is required")
}
direction, ok := unifiedReq.Direction.(string)
if !ok {
return nil, fmt.Errorf("direction must be a string")
}
// Validate direction
validDirections := []string{"up", "down", "left", "right"}
if !slices.Contains(validDirections, direction) {
return nil, fmt.Errorf("invalid swipe direction: %s, expected one of: %v",
direction, validDirections)
}
// Get area coordinates - use SIM-prefixed fields
simAreaStartX := unifiedReq.SIMAreaStartX
simAreaStartY := unifiedReq.SIMAreaStartY
simAreaEndX := unifiedReq.SIMAreaEndX
simAreaEndY := unifiedReq.SIMAreaEndY
simMinDistance := unifiedReq.SIMMinDistance
simMaxDistance := unifiedReq.SIMMaxDistance
// Default values
if simMinDistance == 0 {
simMinDistance = 100
}
if simMaxDistance == 0 {
simMaxDistance = 300
}
log.Info().
Str("direction", direction).
Float64("areaStartX", simAreaStartX).
Float64("areaStartY", simAreaStartY).
Float64("areaEndX", simAreaEndX).
Float64("areaEndY", simAreaEndY).
Float64("minDistance", simMinDistance).
Float64("maxDistance", simMaxDistance).
Msg("performing simulated swipe in area")
// Build all options from request arguments
opts := unifiedReq.Options()
// Call the underlying SIMSwipeInArea method (check if driver supports SIM)
if simDriver, ok := driverExt.IDriver.(SIMSupport); ok {
err = simDriver.SIMSwipeInArea(direction, simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY, simMinDistance, simMaxDistance, opts...)
if err != nil {
return NewMCPErrorResponse(fmt.Sprintf("Simulated swipe in area failed: %s", err.Error())), err
}
} else {
return NewMCPErrorResponse("SIMSwipeInArea is not supported by the current driver"), fmt.Errorf("driver does not implement SIMSupport interface")
}
message := fmt.Sprintf("Successfully performed simulated swipe %s in area (%.2f,%.2f)-(%.2f,%.2f)",
direction, simAreaStartX, simAreaStartY, simAreaEndX, simAreaEndY)
returnData := ToolSIMSwipeInArea{
Direction: direction,
AreaStartX: simAreaStartX,
AreaStartY: simAreaStartY,
AreaEndX: simAreaEndX,
AreaEndY: simAreaEndY,
MinDistance: simMinDistance,
MaxDistance: simMaxDistance,
}
return NewMCPSuccessResponse(message, &returnData), nil
}
}
func (t *ToolSIMSwipeInArea) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
// Handle params as map[string]interface{}
if paramsMap, ok := action.Params.(map[string]interface{}); ok {
arguments := map[string]any{}
// Extract direction
if direction, exists := paramsMap["direction"]; exists {
arguments["direction"] = direction
}
// Extract area coordinates and distances - use SIM-prefixed field names
if areaStartX, exists := paramsMap["sim_area_start_x"]; exists {
arguments["sim_area_start_x"] = areaStartX
}
if areaStartY, exists := paramsMap["sim_area_start_y"]; exists {
arguments["sim_area_start_y"] = areaStartY
}
if areaEndX, exists := paramsMap["sim_area_end_x"]; exists {
arguments["sim_area_end_x"] = areaEndX
}
if areaEndY, exists := paramsMap["sim_area_end_y"]; exists {
arguments["sim_area_end_y"] = areaEndY
}
if minDistance, exists := paramsMap["sim_min_distance"]; exists {
arguments["sim_min_distance"] = minDistance
}
if maxDistance, exists := paramsMap["sim_max_distance"]; exists {
arguments["sim_max_distance"] = maxDistance
}
// 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 BuildMCPCallToolRequest(t.Name(), arguments, action), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid SIM swipe in area params: %v", action.Params)
}
// ToolSIMSwipeFromPointToPoint implements the sim_swipe_point_to_point tool call.
type ToolSIMSwipeFromPointToPoint struct {
// Return data fields - these define the structure of data returned by this tool
StartX float64 `json:"startX" desc:"Starting X coordinate"`
StartY float64 `json:"startY" desc:"Starting Y coordinate"`
EndX float64 `json:"endX" desc:"Ending X coordinate"`
EndY float64 `json:"endY" desc:"Ending Y coordinate"`
}
func (t *ToolSIMSwipeFromPointToPoint) Name() option.ActionName {
return option.ACTION_SIMSwipeFromPointToPoint
}
func (t *ToolSIMSwipeFromPointToPoint) Description() string {
return "Perform simulated swipe from point to point with human-like touch patterns"
}
func (t *ToolSIMSwipeFromPointToPoint) Options() []mcp.ToolOption {
unifiedReq := &option.ActionOptions{}
return unifiedReq.GetMCPOptions(option.ACTION_SIMSwipeFromPointToPoint)
}
func (t *ToolSIMSwipeFromPointToPoint) Implement() server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.GetArguments()
driverExt, err := setupXTDriver(ctx, arguments)
if err != nil {
return nil, fmt.Errorf("setup driver failed: %w", err)
}
unifiedReq, err := parseActionOptions(arguments)
if err != nil {
return nil, err
}
// Get coordinates from arguments - use fromX/fromY instead of startX/startY
fromX := unifiedReq.FromX
fromY := unifiedReq.FromY
toX := unifiedReq.ToX
toY := unifiedReq.ToY
log.Info().
Float64("startX", fromX).
Float64("startY", fromY).
Float64("endX", toX).
Float64("endY", toY).
Msg("performing simulated point to point swipe")
// Build all options from request arguments
opts := unifiedReq.Options()
// Call the underlying SIMSwipeFromPointToPoint method (check if driver supports SIM)
if simDriver, ok := driverExt.IDriver.(SIMSupport); ok {
err = simDriver.SIMSwipeFromPointToPoint(fromX, fromY, toX, toY, opts...)
if err != nil {
return NewMCPErrorResponse(fmt.Sprintf("Simulated point to point swipe failed: %s", err.Error())), err
}
} else {
return NewMCPErrorResponse("SIMSwipeFromPointToPoint is not supported by the current driver"), fmt.Errorf("driver does not implement SIMSupport interface")
}
message := fmt.Sprintf("Successfully performed simulated swipe from (%.2f,%.2f) to (%.2f,%.2f)",
fromX, fromY, toX, toY)
returnData := ToolSIMSwipeFromPointToPoint{
StartX: fromX,
StartY: fromY,
EndX: toX,
EndY: toY,
}
return NewMCPSuccessResponse(message, &returnData), nil
}
}
func (t *ToolSIMSwipeFromPointToPoint) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
// Handle params as map[string]interface{}
if paramsMap, ok := action.Params.(map[string]interface{}); ok {
arguments := map[string]any{}
// Extract coordinates - use new field names directly
if fromX, exists := paramsMap["from_x"]; exists {
arguments["from_x"] = fromX
}
if fromY, exists := paramsMap["from_y"]; exists {
arguments["from_y"] = fromY
}
if toX, exists := paramsMap["to_x"]; exists {
arguments["to_x"] = toX
}
if toY, exists := paramsMap["to_y"]; exists {
arguments["to_y"] = toY
}
// 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 BuildMCPCallToolRequest(t.Name(), arguments, action), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid SIM swipe point to point params: %v", action.Params)
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt/option"
@@ -341,3 +342,95 @@ func (t *ToolDoubleTapXY) ConvertActionToCallToolRequest(action option.MobileAct
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid double tap params: %v", action.Params)
}
// ToolSIMClickAtPoint implements the sim_click_at_point tool call.
type ToolSIMClickAtPoint struct {
// Return data fields - these define the structure of data returned by this tool
X float64 `json:"x" desc:"X coordinate where simulated click was performed"`
Y float64 `json:"y" desc:"Y coordinate where simulated click was performed"`
}
func (t *ToolSIMClickAtPoint) Name() option.ActionName {
return option.ACTION_SIMClickAtPoint
}
func (t *ToolSIMClickAtPoint) Description() string {
return "Perform simulated click at specified point with human-like touch patterns"
}
func (t *ToolSIMClickAtPoint) Options() []mcp.ToolOption {
unifiedReq := &option.ActionOptions{}
return unifiedReq.GetMCPOptions(option.ACTION_SIMClickAtPoint)
}
func (t *ToolSIMClickAtPoint) Implement() server.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.GetArguments()
driverExt, err := setupXTDriver(ctx, arguments)
if err != nil {
return nil, fmt.Errorf("setup driver failed: %w", err)
}
unifiedReq, err := parseActionOptions(arguments)
if err != nil {
return nil, err
}
// Validate required parameters
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
return nil, fmt.Errorf("x and y coordinates are required")
}
x := unifiedReq.X
y := unifiedReq.Y
log.Info().
Float64("x", x).
Float64("y", y).
Msg("performing simulated click at point")
// Build all options from request arguments
opts := unifiedReq.Options()
// Call the underlying SIMClickAtPoint method (check if driver supports SIM)
if simDriver, ok := driverExt.IDriver.(SIMSupport); ok {
err = simDriver.SIMClickAtPoint(x, y, opts...)
if err != nil {
return NewMCPErrorResponse(fmt.Sprintf("Simulated click failed: %s", err.Error())), err
}
} else {
return NewMCPErrorResponse("SIMClickAtPoint is not supported by the current driver"), fmt.Errorf("driver does not implement SIMSupport interface")
}
message := fmt.Sprintf("Successfully performed simulated click at (%.2f, %.2f)", x, y)
returnData := ToolSIMClickAtPoint{
X: x,
Y: y,
}
return NewMCPSuccessResponse(message, &returnData), nil
}
}
func (t *ToolSIMClickAtPoint) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
// Handle params as map[string]interface{}
if paramsMap, ok := action.Params.(map[string]interface{}); ok {
arguments := map[string]any{}
// Extract coordinates
if x, exists := paramsMap["x"]; exists {
arguments["x"] = x
}
if y, exists := paramsMap["y"]; exists {
arguments["y"] = y
}
// Add duration from options
if duration := action.ActionOptions.Duration; duration > 0 {
arguments["duration"] = duration
}
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
}
return mcp.CallToolRequest{}, fmt.Errorf("invalid SIM click at point params: %v", action.Params)
}

View File

@@ -66,9 +66,14 @@ const (
ACTION_TapByCV ActionName = "tap_cv"
ACTION_DoubleTap ActionName = "double_tap" // generic double tap action
ACTION_DoubleTapXY ActionName = "double_tap_xy"
ACTION_Swipe ActionName = "swipe" // swipe by direction or coordinates
ACTION_SwipeDirection ActionName = "swipe_direction" // swipe by direction (up, down, left, right)
ACTION_SwipeCoordinate ActionName = "swipe_coordinate" // swipe by coordinates (fromX, fromY, toX, toY)
ACTION_Swipe ActionName = "swipe" // swipe by direction or coordinates
ACTION_SwipeDirection ActionName = "swipe_direction" // swipe by direction (up, down, left, right)
ACTION_SwipeCoordinate ActionName = "swipe_coordinate" // swipe by coordinates (fromX, fromY, toX, toY)
ACTION_SIMSwipeDirection ActionName = "sim_swipe_direction" // simulated swipe by direction with random distance
ACTION_SIMSwipeInArea ActionName = "sim_swipe_in_area" // simulated swipe in area with direction and distance
ACTION_SIMSwipeFromPointToPoint ActionName = "sim_swipe_point_to_point" // simulated swipe from point to point
ACTION_SIMClickAtPoint ActionName = "sim_click_at_point" // simulated click at point
ACTION_SIMInput ActionName = "sim_input" // simulated text input with segments
ACTION_Drag ActionName = "drag"
ACTION_Input ActionName = "input"
ACTION_PressButton ActionName = "press_button"
@@ -201,9 +206,18 @@ type ActionOptions struct {
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 for action execution"`
TimeLimit int `json:"time_limit,omitempty" yaml:"time_limit,omitempty" desc:"Time limit in seconds for action execution, stops gracefully when reached"`
Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty" desc:"Action frequency"`
// SIM specific options with SIM prefix
SIMMinDistance float64 `json:"sim_min_distance,omitempty" yaml:"sim_min_distance,omitempty" desc:"Minimum distance for SIM simulated actions"`
SIMMaxDistance float64 `json:"sim_max_distance,omitempty" yaml:"sim_max_distance,omitempty" desc:"Maximum distance for SIM simulated actions"`
SIMAreaStartX float64 `json:"sim_area_start_x,omitempty" yaml:"sim_area_start_x,omitempty" desc:"Area starting X coordinate for SIM simulated swipe"`
SIMAreaStartY float64 `json:"sim_area_start_y,omitempty" yaml:"sim_area_start_y,omitempty" desc:"Area starting Y coordinate for SIM simulated swipe"`
SIMAreaEndX float64 `json:"sim_area_end_x,omitempty" yaml:"sim_area_end_x,omitempty" desc:"Area ending X coordinate for SIM simulated swipe"`
SIMAreaEndY float64 `json:"sim_area_end_y,omitempty" yaml:"sim_area_end_y,omitempty" desc:"Area ending Y coordinate for SIM simulated swipe"`
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty" desc:"Timeout in seconds for action execution"`
TimeLimit int `json:"time_limit,omitempty" yaml:"time_limit,omitempty" desc:"Time limit in seconds for action execution, stops gracefully when reached"`
Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty" desc:"Action frequency"`
ScreenOptions
@@ -649,6 +663,13 @@ func (o *ActionOptions) GetMCPOptions(actionType ActionName) []mcp.ToolOption {
ACTION_Back: {"platform", "serial"},
ACTION_ListPackages: {"platform", "serial"},
ACTION_ClosePopups: {"platform", "serial"},
// SIM specific actions using fromX/fromY for startX/startY and SIM-prefixed fields
ACTION_SIMSwipeDirection: {"platform", "serial", "direction", "fromX", "fromY", "sim_min_distance", "sim_max_distance", "duration", "pressDuration"},
ACTION_SIMSwipeInArea: {"platform", "serial", "direction", "sim_area_start_x", "sim_area_start_y", "sim_area_end_x", "sim_area_end_y", "sim_min_distance", "sim_max_distance", "duration", "pressDuration"},
ACTION_SIMSwipeFromPointToPoint: {"platform", "serial", "fromX", "fromY", "toX", "toY", "duration", "pressDuration"},
ACTION_SIMClickAtPoint: {"platform", "serial", "x", "y", "duration", "pressDuration"},
ACTION_SIMInput: {"platform", "serial", "text", "frequency"},
}
fields := fieldMappings[actionType]