mirror of
https://github.com/httprunner/httprunner.git
synced 2026-08-29 20:17:12 +08:00
Merge branch 'master' into wings_interface_merge
This commit is contained in:
@@ -2,10 +2,12 @@ package uitest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
hrp "github.com/httprunner/httprunner/v5"
|
||||||
"github.com/httprunner/httprunner/v5/uixt"
|
"github.com/httprunner/httprunner/v5/uixt"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
@@ -224,3 +226,379 @@ func TestTouchEventSequenceValidation(t *testing.T) {
|
|||||||
|
|
||||||
t.Logf("Touch sequence validation passed: %d events with correct action sequence", len(events))
|
t.Logf("Touch sequence validation passed: %d events with correct action sequence", len(events))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSwipeWithDirection(t *testing.T) {
|
||||||
|
device, err := uixt.NewAndroidDevice(
|
||||||
|
option.WithSerialNumber(""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := uixt.NewUIA2Driver(device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.TearDown()
|
||||||
|
|
||||||
|
// Test cases for different directions and distance configurations
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
direction string
|
||||||
|
startX float64
|
||||||
|
startY float64
|
||||||
|
minDistance float64
|
||||||
|
maxDistance float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "随机距离上滑",
|
||||||
|
direction: "up",
|
||||||
|
startX: 0.5,
|
||||||
|
startY: 0.5,
|
||||||
|
minDistance: 100.0,
|
||||||
|
maxDistance: 500.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := driver.SIMSwipeWithDirection(
|
||||||
|
tc.direction,
|
||||||
|
tc.startX,
|
||||||
|
tc.startY,
|
||||||
|
tc.minDistance,
|
||||||
|
tc.maxDistance,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SwipeWithDirection failed: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully executed swipe: direction=%s, start=(%.1f,%.1f), distance=%.1f-%.1f",
|
||||||
|
tc.direction, tc.startX, tc.startY, tc.minDistance, tc.maxDistance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwipeWithDirectionInvalidInputs(t *testing.T) {
|
||||||
|
device, err := uixt.NewAndroidDevice(
|
||||||
|
option.WithSerialNumber(""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := uixt.NewUIA2Driver(device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.TearDown()
|
||||||
|
|
||||||
|
// Test invalid direction
|
||||||
|
err = driver.SIMSwipeWithDirection("invalid", 500.0, 500.0, 100.0, 200.0)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for invalid direction, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test invalid distance range (max < min)
|
||||||
|
err = driver.SIMSwipeWithDirection("up", 500.0, 500.0, 200.0, 100.0)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for invalid distance range, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test zero distance
|
||||||
|
err = driver.SIMSwipeWithDirection("up", 500.0, 500.0, 0.0, 0.0)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for zero distance, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Invalid input validation tests passed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwipeInArea(t *testing.T) {
|
||||||
|
device, err := uixt.NewAndroidDevice(
|
||||||
|
option.WithSerialNumber(""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := uixt.NewUIA2Driver(device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.TearDown()
|
||||||
|
|
||||||
|
// Test cases for different area configurations and directions
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
direction string
|
||||||
|
areaStartX float64
|
||||||
|
areaStartY float64
|
||||||
|
areaEndX float64
|
||||||
|
areaEndY float64
|
||||||
|
minDistance float64
|
||||||
|
maxDistance float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "中心区域上滑_固定距离",
|
||||||
|
direction: "up",
|
||||||
|
areaStartX: 0.2,
|
||||||
|
areaStartY: 0.3,
|
||||||
|
areaEndX: 0.8,
|
||||||
|
areaEndY: 0.6,
|
||||||
|
minDistance: 500.0,
|
||||||
|
maxDistance: 700.0, // 固定距离
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := driver.SIMSwipeInArea(
|
||||||
|
tc.direction,
|
||||||
|
tc.areaStartX,
|
||||||
|
tc.areaStartY,
|
||||||
|
tc.areaEndX,
|
||||||
|
tc.areaEndY,
|
||||||
|
tc.minDistance,
|
||||||
|
tc.maxDistance,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SwipeInArea failed: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully executed area swipe: direction=%s, area=(%.1f,%.1f)-(%.1f,%.1f), distance=%.1f-%.1f",
|
||||||
|
tc.direction, tc.areaStartX, tc.areaStartY, tc.areaEndX, tc.areaEndY, tc.minDistance, tc.maxDistance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwipeFromPointToPoint(t *testing.T) {
|
||||||
|
device, err := uixt.NewAndroidDevice(
|
||||||
|
option.WithSerialNumber(""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := uixt.NewUIA2Driver(device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.TearDown()
|
||||||
|
|
||||||
|
// Test cases for different point-to-point swipes
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
startX float64
|
||||||
|
startY float64
|
||||||
|
endX float64
|
||||||
|
endY float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "对角线滑动_左上到右下",
|
||||||
|
startX: 0.2,
|
||||||
|
startY: 0.3,
|
||||||
|
endX: 0.8,
|
||||||
|
endY: 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := driver.SIMSwipeFromPointToPoint(
|
||||||
|
tc.startX,
|
||||||
|
tc.startY,
|
||||||
|
tc.endX,
|
||||||
|
tc.endY,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SwipeFromPointToPoint failed: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully executed point-to-point swipe: %s, from (%.1f,%.1f) to (%.1f,%.1f)",
|
||||||
|
tc.name, tc.startX, tc.startY, tc.endX, tc.endY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwipeFromPointToPointInvalidInputs(t *testing.T) {
|
||||||
|
device, err := uixt.NewAndroidDevice(
|
||||||
|
option.WithSerialNumber(""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := uixt.NewUIA2Driver(device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.TearDown()
|
||||||
|
|
||||||
|
// Test same start and end point
|
||||||
|
err = driver.SIMSwipeFromPointToPoint(0.5, 0.5, 0.5, 0.5)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for same start and end point, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test very close points (should result in distance too short)
|
||||||
|
err = driver.SIMSwipeFromPointToPoint(0.5, 0.5, 0.501, 0.501)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for very close points, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Point-to-point swipe invalid input validation tests passed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClickAtPoint(t *testing.T) {
|
||||||
|
device, err := uixt.NewAndroidDevice(
|
||||||
|
option.WithSerialNumber(""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := uixt.NewUIA2Driver(device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.TearDown()
|
||||||
|
|
||||||
|
// Test cases for different click positions
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
x float64
|
||||||
|
y float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "屏幕中心点击",
|
||||||
|
x: 0.5,
|
||||||
|
y: 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := driver.SIMClickAtPoint(tc.x, tc.y)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ClickAtPoint failed: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully executed click: %s at (%.1f, %.1f)",
|
||||||
|
tc.name, tc.x, tc.y)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClickAtPointInvalidInputs(t *testing.T) {
|
||||||
|
device, err := uixt.NewAndroidDevice(
|
||||||
|
option.WithSerialNumber(""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := uixt.NewUIA2Driver(device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.TearDown()
|
||||||
|
|
||||||
|
// Test negative coordinates
|
||||||
|
err = driver.SIMClickAtPoint(-0.1, 0.5)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for negative x coordinate, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = driver.SIMClickAtPoint(0.5, -0.1)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for negative y coordinate, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test coordinates out of range (though these should be handled by convertToAbsolutePoint)
|
||||||
|
err = driver.SIMClickAtPoint(1.5, 0.5)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Out of range coordinates handled properly: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Click invalid input validation tests passed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSIMInput(t *testing.T) {
|
||||||
|
device, err := uixt.NewAndroidDevice(
|
||||||
|
option.WithSerialNumber(""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := uixt.NewUIA2Driver(device)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.TearDown()
|
||||||
|
|
||||||
|
// Test cases for different text inputs
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
text string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "长文本",
|
||||||
|
text: "This is a very long text to test the performance of SIMInput function. 这是一个很长的文本用来测试SIMInput函数的性能。1234567890!@#$%^&*()英語の長い文字",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := driver.SIMInput(tc.text)
|
||||||
|
// err := driver.Input(tc.text)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SIMInput failed: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully executed SIMInput: %s with text '%s'", tc.name, tc.text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -64,7 +64,7 @@ func NewGA4Client(measurementID, apiSecret string, debug ...bool) *GA4Client {
|
|||||||
apiSecret: apiSecret,
|
apiSecret: apiSecret,
|
||||||
userID: userID,
|
userID: userID,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
},
|
},
|
||||||
debug: dbg,
|
debug: dbg,
|
||||||
}
|
}
|
||||||
@@ -206,6 +206,6 @@ func SendGA4Event(name string, params map[string]interface{}) {
|
|||||||
}
|
}
|
||||||
err := ga4Client.SendEvent(event)
|
err := ga4Client.SendEvent(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("send GA4 event failed")
|
log.Debug().Err(err).Msg("send GA4 event failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,563 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClickRequest 点击请求参数
|
||||||
|
type ClickRequest struct {
|
||||||
|
X float64 `json:"x"` // 点击X坐标
|
||||||
|
Y float64 `json:"y"` // 点击Y坐标
|
||||||
|
DeviceID int `json:"device_id"` // 设备ID
|
||||||
|
Pressure float64 `json:"pressure"` // 压力值
|
||||||
|
Size float64 `json:"size"` // 接触面积
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClickResponse 点击响应结果
|
||||||
|
type ClickResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Points []ClickPoint `json:"points"`
|
||||||
|
Metrics ClickMetrics `json:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClickMetrics 点击指标
|
||||||
|
type ClickMetrics struct {
|
||||||
|
TotalDuration int64 `json:"total_duration_ms"` // 总持续时间(毫秒)
|
||||||
|
PointCount int `json:"point_count"` // 轨迹点数量
|
||||||
|
MaxDeviation float64 `json:"max_deviation"` // 最大偏移距离
|
||||||
|
AverageInterval float64 `json:"average_interval_ms"` // 平均采样间隔
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClickPoint 点击轨迹点
|
||||||
|
type ClickPoint struct {
|
||||||
|
Timestamp int64 `json:"timestamp"` // 时间戳(毫秒)
|
||||||
|
X float64 `json:"x"` // X坐标
|
||||||
|
Y float64 `json:"y"` // Y坐标
|
||||||
|
DeviceID int `json:"device_id"` // 设备ID
|
||||||
|
Pressure float64 `json:"pressure"` // 压力值
|
||||||
|
Size float64 `json:"size"` // 接触面积
|
||||||
|
Action int `json:"action"` // 动作类型(0=按下,1=抬起,2=移动)
|
||||||
|
EventTime int64 `json:"event_time"` // 相对第一个点的时间(ms),第一个点为0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClickConfig 点击配置参数
|
||||||
|
type ClickConfig struct {
|
||||||
|
MinDuration int64 // 最小持续时间(毫秒)
|
||||||
|
MaxDuration int64 // 最大持续时间(毫秒)
|
||||||
|
MinPoints int // 最小点数
|
||||||
|
MaxPoints int // 最大点数
|
||||||
|
MaxDeviation float64 // 最大坐标偏移(像素)
|
||||||
|
NoiseLevel float64 // 噪声级别
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultClickConfig 默认配置
|
||||||
|
var DefaultClickConfig = ClickConfig{
|
||||||
|
MinDuration: 40,
|
||||||
|
MaxDuration: 90,
|
||||||
|
MinPoints: 4, // 增加最小点数从3到4,确保至少有2个MOVE事件
|
||||||
|
MaxPoints: 6,
|
||||||
|
MaxDeviation: 2.0,
|
||||||
|
NoiseLevel: 0.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClickSimulatorAPI 点击仿真API
|
||||||
|
type ClickSimulatorAPI struct {
|
||||||
|
rand *rand.Rand
|
||||||
|
config ClickConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCase 测试用例
|
||||||
|
type ClickTestCase struct {
|
||||||
|
Name string
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
DeviceID int
|
||||||
|
Pressure float64
|
||||||
|
Size float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClickSimulatorAPI 创建新的点击仿真API
|
||||||
|
func NewClickSimulatorAPI(config *ClickConfig) *ClickSimulatorAPI {
|
||||||
|
if config == nil {
|
||||||
|
config = &DefaultClickConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ClickSimulatorAPI{
|
||||||
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
config: *config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateClick 生成点击轨迹
|
||||||
|
func (api *ClickSimulatorAPI) GenerateClick(req ClickRequest) ClickResponse {
|
||||||
|
// 验证输入参数
|
||||||
|
if err := api.validateRequest(req); err != nil {
|
||||||
|
return ClickResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成点击轨迹
|
||||||
|
points := api.generateClickPoints(req)
|
||||||
|
|
||||||
|
// 计算指标
|
||||||
|
metrics := api.calculateMetrics(points, req.X, req.Y)
|
||||||
|
|
||||||
|
return ClickResponse{
|
||||||
|
Success: true,
|
||||||
|
Points: points,
|
||||||
|
Metrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateRequest 验证请求参数
|
||||||
|
func (api *ClickSimulatorAPI) validateRequest(req ClickRequest) error {
|
||||||
|
if req.X < 0 || req.Y < 0 {
|
||||||
|
return fmt.Errorf("coordinates must be non-negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DeviceID < 0 {
|
||||||
|
return fmt.Errorf("device_id must be non-negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateClickPoints 生成点击轨迹点
|
||||||
|
func (api *ClickSimulatorAPI) generateClickPoints(req ClickRequest) []ClickPoint {
|
||||||
|
// 计算点击参数
|
||||||
|
duration := api.calculateDuration()
|
||||||
|
pointCount := api.calculatePointCount()
|
||||||
|
|
||||||
|
// 生成时间戳序列
|
||||||
|
timestamps := api.generateTimestamps(duration, pointCount)
|
||||||
|
|
||||||
|
// 生成轨迹点
|
||||||
|
points := make([]ClickPoint, pointCount)
|
||||||
|
|
||||||
|
// 生成size变化曲线(基于真实数据分析)
|
||||||
|
sizeValues := api.generateSizeValues(pointCount, req.Size)
|
||||||
|
|
||||||
|
// 生成压力值序列
|
||||||
|
pressureValues := api.generatePressureValues(pointCount, req.Pressure)
|
||||||
|
|
||||||
|
// 生成坐标偏移序列
|
||||||
|
xOffsets, yOffsets := api.generateCoordinateOffsets(pointCount)
|
||||||
|
|
||||||
|
baseTimestamp := timestamps[0]
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
// 计算当前坐标(添加轻微偏移)
|
||||||
|
currentX := req.X + xOffsets[i]
|
||||||
|
currentY := req.Y + yOffsets[i]
|
||||||
|
|
||||||
|
// 确定动作类型
|
||||||
|
var action int
|
||||||
|
if i == 0 {
|
||||||
|
action = 0 // 按下
|
||||||
|
} else if i == pointCount-1 {
|
||||||
|
action = 1 // 抬起
|
||||||
|
} else {
|
||||||
|
action = 2 // 移动
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTime := timestamps[i] - baseTimestamp
|
||||||
|
|
||||||
|
points[i] = ClickPoint{
|
||||||
|
Timestamp: timestamps[i],
|
||||||
|
X: currentX,
|
||||||
|
Y: currentY,
|
||||||
|
DeviceID: req.DeviceID,
|
||||||
|
Pressure: pressureValues[i],
|
||||||
|
Size: sizeValues[i],
|
||||||
|
Action: action,
|
||||||
|
EventTime: eventTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatePressureValues 生成pressure值序列,基于用户输入的压力值动态仿真点击操作
|
||||||
|
func (api *ClickSimulatorAPI) generatePressureValues(pointCount int, basePressure float64) []float64 {
|
||||||
|
pressures := make([]float64, pointCount)
|
||||||
|
|
||||||
|
// 如果用户没有提供压力值,使用默认值
|
||||||
|
if basePressure <= 0 {
|
||||||
|
basePressure = 1 // 默认压力值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特殊处理:当压力值为1时,保持恒定不变
|
||||||
|
if basePressure == 1 {
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
pressures[i] = 1.0
|
||||||
|
}
|
||||||
|
return pressures
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将整数压力值转换为浮点数
|
||||||
|
baseP := float64(basePressure)
|
||||||
|
|
||||||
|
// 基于真实点击数据观察的压力变化规律:
|
||||||
|
// 点击操作的pressure变化特点:快速上升→短暂保持峰值→快速下降
|
||||||
|
// 1. 起始压力:基础压力的95%-105%
|
||||||
|
// 2. 峰值压力:基础压力的102%-108% (相对滑动,点击的峰值增幅较小)
|
||||||
|
// 3. 结束压力:基础压力的25%-35% (快速下降到较低值)
|
||||||
|
|
||||||
|
startPressureRatio := 0.95 + api.rand.Float64()*0.10 // 95%-105%
|
||||||
|
peakPressureRatio := 1.02 + api.rand.Float64()*0.06 // 102%-108%
|
||||||
|
endPressureRatio := 0.25 + api.rand.Float64()*0.10 // 25%-35%
|
||||||
|
|
||||||
|
startPressure := baseP * startPressureRatio
|
||||||
|
peakPressure := baseP * peakPressureRatio
|
||||||
|
endPressure := baseP * endPressureRatio
|
||||||
|
|
||||||
|
// 点击操作的峰值通常出现在早期(第2-3个点)
|
||||||
|
var peakIndex int
|
||||||
|
if pointCount <= 3 {
|
||||||
|
peakIndex = 1 // 对于短序列,峰值在第2个点
|
||||||
|
} else {
|
||||||
|
peakIndex = 1 + api.rand.Intn(2) // 峰值在第2或第3个点
|
||||||
|
}
|
||||||
|
if peakIndex >= pointCount {
|
||||||
|
peakIndex = pointCount - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保压力值在合理范围内(0.5-15.0)
|
||||||
|
//if startPressure < 0.5 {
|
||||||
|
// startPressure = 0.5
|
||||||
|
//}
|
||||||
|
//if peakPressure > 15.0 {
|
||||||
|
// peakPressure = 15.0
|
||||||
|
//}
|
||||||
|
//if endPressure < 0.5 {
|
||||||
|
// endPressure = 0.5
|
||||||
|
//}
|
||||||
|
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
var pressure float64
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
// 第一个点:起始压力
|
||||||
|
pressure = startPressure
|
||||||
|
} else if i <= peakIndex {
|
||||||
|
// 上升到峰值阶段
|
||||||
|
pressure = peakPressure
|
||||||
|
} else if i == pointCount-1 {
|
||||||
|
// 最后一个点:结束压力
|
||||||
|
pressure = endPressure
|
||||||
|
} else {
|
||||||
|
// 从峰值下降到结束压力的过渡阶段
|
||||||
|
t := float64(i-peakIndex) / float64(pointCount-1-peakIndex)
|
||||||
|
pressure = peakPressure + (endPressure-peakPressure)*t
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加随机噪声(±3%),点击操作的噪声相对较小
|
||||||
|
noiseRange := pressure * 0.03
|
||||||
|
noise := (api.rand.Float64() - 0.5) * noiseRange
|
||||||
|
pressure += noise
|
||||||
|
|
||||||
|
// 确保pressure在合理范围内
|
||||||
|
//if pressure < 0.5 {
|
||||||
|
// pressure = 0.5 + api.rand.Float64()*0.3
|
||||||
|
//}
|
||||||
|
//if pressure > 15.0 {
|
||||||
|
// pressure = 14.5 + api.rand.Float64()*0.5
|
||||||
|
//}
|
||||||
|
|
||||||
|
// 保留两位小数精度
|
||||||
|
pressures[i] = math.Round(pressure*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return pressures
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateDuration 计算点击持续时间
|
||||||
|
func (api *ClickSimulatorAPI) calculateDuration() int64 {
|
||||||
|
// 基于真实数据的持续时间算法
|
||||||
|
baseDuration := float64(api.config.MinDuration+api.config.MaxDuration) / 2
|
||||||
|
randomFactor := api.rand.Float64()*float64(api.config.MaxDuration-api.config.MinDuration) -
|
||||||
|
float64(api.config.MaxDuration-api.config.MinDuration)/2
|
||||||
|
|
||||||
|
duration := baseDuration + randomFactor
|
||||||
|
|
||||||
|
if duration < float64(api.config.MinDuration) {
|
||||||
|
duration = float64(api.config.MinDuration)
|
||||||
|
}
|
||||||
|
if duration > float64(api.config.MaxDuration) {
|
||||||
|
duration = float64(api.config.MaxDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculatePointCount 计算轨迹点数量
|
||||||
|
func (api *ClickSimulatorAPI) calculatePointCount() int {
|
||||||
|
// 基于真实数据分析,点击通常有3-6个点
|
||||||
|
count := api.config.MinPoints + api.rand.Intn(api.config.MaxPoints-api.config.MinPoints+1)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateTimestamps 生成时间戳序列
|
||||||
|
func (api *ClickSimulatorAPI) generateTimestamps(duration int64, pointCount int) []int64 {
|
||||||
|
baseTime := time.Now().UnixMilli()
|
||||||
|
timestamps := make([]int64, pointCount)
|
||||||
|
|
||||||
|
timestamps[0] = baseTime
|
||||||
|
|
||||||
|
if pointCount == 1 {
|
||||||
|
return timestamps
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于真实数据的时间间隔模式
|
||||||
|
for i := 1; i < pointCount; i++ {
|
||||||
|
// 时间间隔:前期较短,后期可能较长
|
||||||
|
var intervalRatio float64
|
||||||
|
if i == 1 {
|
||||||
|
// 第一个间隔较短 (8-30ms)
|
||||||
|
intervalRatio = 0.1 + api.rand.Float64()*0.2 // 10%-30%
|
||||||
|
} else if i == pointCount-1 {
|
||||||
|
// 最后一个间隔可能较短
|
||||||
|
intervalRatio = 0.1 + api.rand.Float64()*0.15 // 10%-25%
|
||||||
|
} else {
|
||||||
|
// 中间间隔相对均匀
|
||||||
|
intervalRatio = 0.15 + api.rand.Float64()*0.25 // 15%-40%
|
||||||
|
}
|
||||||
|
|
||||||
|
interval := int64(float64(duration) * intervalRatio)
|
||||||
|
timestamps[i] = timestamps[i-1] + interval
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保最后一个时间戳不超过总持续时间
|
||||||
|
if timestamps[pointCount-1] > baseTime+duration {
|
||||||
|
timestamps[pointCount-1] = baseTime + duration
|
||||||
|
}
|
||||||
|
|
||||||
|
return timestamps
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSizeValues 生成size值序列,基于真实数据分析
|
||||||
|
func (api *ClickSimulatorAPI) generateSizeValues(pointCount int, baseSize float64) []float64 {
|
||||||
|
sizes := make([]float64, pointCount)
|
||||||
|
|
||||||
|
// 如果baseSize为0,使用默认值
|
||||||
|
if baseSize == 0 {
|
||||||
|
baseSize = 0.043 // 默认size值,基于真实数据平均值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态计算size范围,基于baseSize的值来适应不同设备
|
||||||
|
var minSize, maxSize float64
|
||||||
|
if baseSize < 1.0 {
|
||||||
|
// 小数值范围(如0.043),使用原有逻辑
|
||||||
|
minSize = 0.035
|
||||||
|
maxSize = 0.051
|
||||||
|
// 确保baseSize在合理范围内
|
||||||
|
if baseSize < minSize {
|
||||||
|
baseSize = minSize + api.rand.Float64()*(maxSize-minSize)*0.3
|
||||||
|
}
|
||||||
|
if baseSize > maxSize {
|
||||||
|
baseSize = maxSize - api.rand.Float64()*(maxSize-minSize)*0.3
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 大数值范围(如几十或几百),基于baseSize动态计算范围
|
||||||
|
// 允许在baseSize的±20%范围内变化
|
||||||
|
minSize = baseSize * 0.8
|
||||||
|
maxSize = baseSize * 1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
// 基础size值随点击进度变化
|
||||||
|
var sizeModifier float64
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
// 开始时:可能较小
|
||||||
|
sizeModifier = 0.85 + api.rand.Float64()*0.3 // 0.85-1.15倍
|
||||||
|
} else if i == pointCount-1 {
|
||||||
|
// 结束时:可能减小(手指抬起)
|
||||||
|
sizeModifier = 0.8 + api.rand.Float64()*0.25 // 0.8-1.05倍
|
||||||
|
} else {
|
||||||
|
// 中间过程:可能增大(压力增加)
|
||||||
|
sizeModifier = 0.95 + api.rand.Float64()*0.25 // 0.95-1.2倍
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用变化
|
||||||
|
sizes[i] = baseSize * sizeModifier
|
||||||
|
|
||||||
|
// 确保在合理范围内
|
||||||
|
if sizes[i] < minSize {
|
||||||
|
sizes[i] = minSize
|
||||||
|
}
|
||||||
|
if sizes[i] > maxSize {
|
||||||
|
sizes[i] = maxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加轻微随机噪声,噪声大小根据baseSize动态调整
|
||||||
|
var noiseLevel float64
|
||||||
|
if baseSize < 1.0 {
|
||||||
|
noiseLevel = 0.002 // 小数值使用固定的小噪声
|
||||||
|
} else {
|
||||||
|
noiseLevel = baseSize * 0.01 // 大数值使用baseSize的1%作为噪声
|
||||||
|
}
|
||||||
|
sizes[i] += api.addNoise(noiseLevel)
|
||||||
|
|
||||||
|
// 最终范围检查
|
||||||
|
if sizes[i] < minSize {
|
||||||
|
sizes[i] = minSize
|
||||||
|
}
|
||||||
|
if sizes[i] > maxSize {
|
||||||
|
sizes[i] = maxSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCoordinateOffsets 生成坐标偏移序列
|
||||||
|
func (api *ClickSimulatorAPI) generateCoordinateOffsets(pointCount int) ([]float64, []float64) {
|
||||||
|
xOffsets := make([]float64, pointCount)
|
||||||
|
yOffsets := make([]float64, pointCount)
|
||||||
|
|
||||||
|
// 第一个点不偏移
|
||||||
|
xOffsets[0] = 0
|
||||||
|
yOffsets[0] = 0
|
||||||
|
|
||||||
|
if pointCount == 1 {
|
||||||
|
return xOffsets, yOffsets
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于真实数据分析,点击时会有轻微的移动
|
||||||
|
for i := 1; i < pointCount; i++ {
|
||||||
|
// 累积偏移,模拟手指的轻微移动
|
||||||
|
maxOffset := api.config.MaxDeviation * float64(i) / float64(pointCount-1)
|
||||||
|
|
||||||
|
// 添加随机偏移
|
||||||
|
xOffsets[i] = xOffsets[i-1] + api.addNoise(maxOffset*0.5)
|
||||||
|
yOffsets[i] = yOffsets[i-1] + api.addNoise(maxOffset*0.5)
|
||||||
|
|
||||||
|
// 限制总偏移量
|
||||||
|
if math.Abs(xOffsets[i]) > api.config.MaxDeviation {
|
||||||
|
if xOffsets[i] > 0 {
|
||||||
|
xOffsets[i] = api.config.MaxDeviation
|
||||||
|
} else {
|
||||||
|
xOffsets[i] = -api.config.MaxDeviation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if math.Abs(yOffsets[i]) > api.config.MaxDeviation {
|
||||||
|
if yOffsets[i] > 0 {
|
||||||
|
yOffsets[i] = api.config.MaxDeviation
|
||||||
|
} else {
|
||||||
|
yOffsets[i] = -api.config.MaxDeviation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return xOffsets, yOffsets
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNoise 添加随机噪声
|
||||||
|
func (api *ClickSimulatorAPI) addNoise(maxNoise float64) float64 {
|
||||||
|
return (api.rand.Float64() - 0.5) * maxNoise * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateMetrics 计算点击指标
|
||||||
|
func (api *ClickSimulatorAPI) calculateMetrics(points []ClickPoint, originX, originY float64) ClickMetrics {
|
||||||
|
if len(points) == 0 {
|
||||||
|
return ClickMetrics{}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDuration := points[len(points)-1].Timestamp - points[0].Timestamp
|
||||||
|
|
||||||
|
// 计算最大偏移距离
|
||||||
|
var maxDeviation float64
|
||||||
|
for _, point := range points {
|
||||||
|
deviation := math.Sqrt((point.X-originX)*(point.X-originX) + (point.Y-originY)*(point.Y-originY))
|
||||||
|
if deviation > maxDeviation {
|
||||||
|
maxDeviation = deviation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算平均间隔
|
||||||
|
var averageInterval float64
|
||||||
|
if len(points) > 1 {
|
||||||
|
averageInterval = float64(totalDuration) / float64(len(points)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClickMetrics{
|
||||||
|
TotalDuration: totalDuration,
|
||||||
|
PointCount: len(points),
|
||||||
|
MaxDeviation: maxDeviation,
|
||||||
|
AverageInterval: averageInterval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON 将结果转换为JSON
|
||||||
|
func (resp ClickResponse) ToJSON() (string, error) {
|
||||||
|
data, err := json.MarshalIndent(resp, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertClickToTouchEvents 将ClickPoint切片转换为TouchEvent切片
|
||||||
|
func (api *ClickSimulatorAPI) ConvertClickToTouchEvents(points []ClickPoint) []types.TouchEvent {
|
||||||
|
if len(points) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
events := make([]types.TouchEvent, len(points))
|
||||||
|
baseDownTime := points[0].Timestamp
|
||||||
|
|
||||||
|
for i, point := range points {
|
||||||
|
events[i] = types.TouchEvent{
|
||||||
|
X: point.X,
|
||||||
|
Y: point.Y,
|
||||||
|
DeviceID: point.DeviceID,
|
||||||
|
Pressure: float64(point.Pressure),
|
||||||
|
Size: point.Size,
|
||||||
|
RawX: point.X, // 使用相同的X坐标
|
||||||
|
RawY: point.Y, // 使用相同的Y坐标
|
||||||
|
DownTime: baseDownTime, // 第一个事件的时间戳作为DownTime
|
||||||
|
EventTime: point.Timestamp,
|
||||||
|
ToolType: 1, // TOOL_TYPE_FINGER
|
||||||
|
Flag: 0, // 默认flag
|
||||||
|
Action: point.Action, // 直接使用point的action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateClickEvents 生成点击的TouchEvent序列
|
||||||
|
func (api *ClickSimulatorAPI) GenerateClickEvents(x, y float64, deviceID int, pressure float64, size float64) ([]types.TouchEvent, error) {
|
||||||
|
// 验证输入参数
|
||||||
|
if x < 0 || y < 0 {
|
||||||
|
return nil, fmt.Errorf("coordinates must be non-negative: x=%.2f, y=%.2f", x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建点击请求
|
||||||
|
req := ClickRequest{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
Pressure: pressure,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成点击轨迹
|
||||||
|
response := api.GenerateClick(req)
|
||||||
|
if !response.Success {
|
||||||
|
return nil, fmt.Errorf("generate click failed: %s", response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为TouchEvent
|
||||||
|
events := api.ConvertClickToTouchEvents(response.Points)
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeviceConfig struct {
|
||||||
|
DeviceID int
|
||||||
|
PressureMin float64
|
||||||
|
PressureMax float64
|
||||||
|
SizeMin float64
|
||||||
|
SizeMax float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceParams 设备参数结构体
|
||||||
|
type DeviceParams struct {
|
||||||
|
DeviceID int
|
||||||
|
Pressure float64
|
||||||
|
Size float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRandomDeviceParams 根据设备型号获取随机的设备参数
|
||||||
|
func GetRandomDeviceParams(deviceModel string) DeviceParams {
|
||||||
|
config := getDeviceConfig(deviceModel)
|
||||||
|
|
||||||
|
// 创建随机数生成器
|
||||||
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
// 在最小值和最大值之间生成随机数
|
||||||
|
randomPressure := config.PressureMin + rng.Float64()*(config.PressureMax-config.PressureMin)
|
||||||
|
randomSize := config.SizeMin + rng.Float64()*(config.SizeMax-config.SizeMin)
|
||||||
|
|
||||||
|
// 保留合理的精度
|
||||||
|
randomPressure = float64(int(randomPressure*100)) / 100 // 保留2位小数
|
||||||
|
randomSize = float64(int(randomSize*1000)) / 1000 // 保留3位小数
|
||||||
|
|
||||||
|
return DeviceParams{
|
||||||
|
DeviceID: config.DeviceID,
|
||||||
|
Pressure: randomPressure,
|
||||||
|
Size: randomSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDeviceConfig returns device-specific configuration based on device model
|
||||||
|
func getDeviceConfig(deviceModel string) DeviceConfig {
|
||||||
|
switch deviceModel {
|
||||||
|
// "HUAWEI"
|
||||||
|
case "SEA-AL00": // 华为nova5
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 1,
|
||||||
|
PressureMin: 1.2,
|
||||||
|
PressureMax: 1.8,
|
||||||
|
SizeMin: 160.0,
|
||||||
|
SizeMax: 200.0,
|
||||||
|
}
|
||||||
|
case "ABR-AL00": // 华为P50
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 3,
|
||||||
|
PressureMin: 1.4,
|
||||||
|
PressureMax: 2.0,
|
||||||
|
SizeMin: 170.0,
|
||||||
|
SizeMax: 220.0,
|
||||||
|
}
|
||||||
|
case "SEA-AL10": // 华为nova5Pro
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 3,
|
||||||
|
PressureMin: 1.3,
|
||||||
|
PressureMax: 1.9,
|
||||||
|
SizeMin: 165.0,
|
||||||
|
SizeMax: 210.0,
|
||||||
|
}
|
||||||
|
case "ANA-AN00": // 华为P40
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 4,
|
||||||
|
PressureMin: 1.5,
|
||||||
|
PressureMax: 2.2,
|
||||||
|
SizeMin: 180.0,
|
||||||
|
SizeMax: 230.0,
|
||||||
|
}
|
||||||
|
case "ELS-AN00": // 华为P40Pro
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 5,
|
||||||
|
PressureMin: 1.6,
|
||||||
|
PressureMax: 2.3,
|
||||||
|
SizeMin: 185.0,
|
||||||
|
SizeMax: 240.0,
|
||||||
|
}
|
||||||
|
case "NCO_AL00":
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 3,
|
||||||
|
PressureMin: 3,
|
||||||
|
PressureMax: 7,
|
||||||
|
SizeMin: 140.0,
|
||||||
|
SizeMax: 200.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Xiaomi"
|
||||||
|
case "M2007J22C": // RedmiNote9 5G
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 3,
|
||||||
|
PressureMin: 1.3,
|
||||||
|
PressureMax: 1.9,
|
||||||
|
SizeMin: 170.0,
|
||||||
|
SizeMax: 215.0,
|
||||||
|
}
|
||||||
|
case "2211133C": // 小米13
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 7,
|
||||||
|
PressureMin: 1.7,
|
||||||
|
PressureMax: 2.4,
|
||||||
|
SizeMin: 190.0,
|
||||||
|
SizeMax: 250.0,
|
||||||
|
}
|
||||||
|
case "2206123SC": // 小米12s
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 8,
|
||||||
|
PressureMin: 1.6,
|
||||||
|
PressureMax: 2.3,
|
||||||
|
SizeMin: 185.0,
|
||||||
|
SizeMax: 245.0,
|
||||||
|
}
|
||||||
|
case "21091116C":
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 5,
|
||||||
|
PressureMin: 1,
|
||||||
|
PressureMax: 1,
|
||||||
|
SizeMin: 0,
|
||||||
|
SizeMax: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Google"
|
||||||
|
case "Pixel 6 Pro":
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 4,
|
||||||
|
PressureMin: 1.4,
|
||||||
|
PressureMax: 2.1,
|
||||||
|
SizeMin: 175.0,
|
||||||
|
SizeMax: 225.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default configuration for unknown devices
|
||||||
|
default:
|
||||||
|
return DeviceConfig{
|
||||||
|
DeviceID: 6,
|
||||||
|
PressureMin: 1.2,
|
||||||
|
PressureMax: 2.0,
|
||||||
|
SizeMin: 160.0,
|
||||||
|
SizeMax: 220.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InputRequest 输入请求参数
|
||||||
|
type InputRequest struct {
|
||||||
|
Text string `json:"text"` // 输入文本
|
||||||
|
MinSegmentLen int `json:"min_segment"` // 最小分割长度
|
||||||
|
MaxSegmentLen int `json:"max_segment"` // 最大分割长度
|
||||||
|
MinDelayMs int `json:"min_delay_ms"` // 最小延迟时间(毫秒)
|
||||||
|
MaxDelayMs int `json:"max_delay_ms"` // 最大延迟时间(毫秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputResponse 输入响应结果
|
||||||
|
type InputResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Segments []InputSegment `json:"segments"`
|
||||||
|
Metrics InputMetrics `json:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputSegment 输入片段
|
||||||
|
type InputSegment struct {
|
||||||
|
Index int `json:"index"` // 片段索引
|
||||||
|
Text string `json:"text"` // 片段文本
|
||||||
|
DelayMs int `json:"delay_ms"` // 该片段后的延迟时间(毫秒)
|
||||||
|
CharLen int `json:"char_len"` // 字符长度
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputMetrics 输入指标
|
||||||
|
type InputMetrics struct {
|
||||||
|
TotalSegments int `json:"total_segments"` // 总片段数
|
||||||
|
TotalDelayMs int `json:"total_delay_ms"` // 总延迟时间
|
||||||
|
EstimatedTimeMs int `json:"estimated_time_ms"` // 预估总耗时
|
||||||
|
OriginalCharLen int `json:"original_char_len"` // 原始字符长度
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputConfig 输入配置参数
|
||||||
|
type InputConfig struct {
|
||||||
|
MinSegmentLen int // 最小分割长度(字符数)
|
||||||
|
MaxSegmentLen int // 最大分割长度(字符数)
|
||||||
|
MinDelayMs int // 最小延迟时间(毫秒)
|
||||||
|
MaxDelayMs int // 最大延迟时间(毫秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultInputConfig 默认输入配置
|
||||||
|
var DefaultInputConfig = InputConfig{
|
||||||
|
MinSegmentLen: 1, // 1个字符
|
||||||
|
MaxSegmentLen: 4, // 4个字符
|
||||||
|
MinDelayMs: 50, // 50毫秒
|
||||||
|
MaxDelayMs: 200, // 200毫秒
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputSimulatorAPI 输入仿真API
|
||||||
|
type InputSimulatorAPI struct {
|
||||||
|
rand *rand.Rand
|
||||||
|
config InputConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInputSimulatorAPI 创建新的输入仿真API
|
||||||
|
func NewInputSimulatorAPI(config *InputConfig) *InputSimulatorAPI {
|
||||||
|
if config == nil {
|
||||||
|
config = &DefaultInputConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InputSimulatorAPI{
|
||||||
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
config: *config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateInputSegments 生成输入片段序列
|
||||||
|
func (api *InputSimulatorAPI) GenerateInputSegments(req InputRequest) InputResponse {
|
||||||
|
// 验证输入参数
|
||||||
|
if err := api.validateRequest(req); err != nil {
|
||||||
|
return InputResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果文本为空,直接返回
|
||||||
|
if req.Text == "" {
|
||||||
|
return InputResponse{
|
||||||
|
Success: true,
|
||||||
|
Segments: []InputSegment{},
|
||||||
|
Metrics: InputMetrics{
|
||||||
|
TotalSegments: 0,
|
||||||
|
TotalDelayMs: 0,
|
||||||
|
EstimatedTimeMs: 0,
|
||||||
|
OriginalCharLen: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成分割片段
|
||||||
|
segments := api.splitTextIntelligently(req.Text, req.MinSegmentLen, req.MaxSegmentLen)
|
||||||
|
|
||||||
|
// 生成延迟时间
|
||||||
|
inputSegments := make([]InputSegment, len(segments))
|
||||||
|
totalDelayMs := 0
|
||||||
|
|
||||||
|
for i, segment := range segments {
|
||||||
|
var delayMs int
|
||||||
|
// 最后一个片段不需要延迟
|
||||||
|
if i < len(segments)-1 {
|
||||||
|
delayMs = api.generateRandomDelay(req.MinDelayMs, req.MaxDelayMs)
|
||||||
|
totalDelayMs += delayMs
|
||||||
|
}
|
||||||
|
|
||||||
|
inputSegments[i] = InputSegment{
|
||||||
|
Index: i,
|
||||||
|
Text: segment,
|
||||||
|
DelayMs: delayMs,
|
||||||
|
CharLen: len([]rune(segment)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算指标
|
||||||
|
metrics := InputMetrics{
|
||||||
|
TotalSegments: len(segments),
|
||||||
|
TotalDelayMs: totalDelayMs,
|
||||||
|
EstimatedTimeMs: totalDelayMs, // 简化计算,实际输入时间可能更长
|
||||||
|
OriginalCharLen: len([]rune(req.Text)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return InputResponse{
|
||||||
|
Success: true,
|
||||||
|
Segments: inputSegments,
|
||||||
|
Metrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateRequest 验证请求参数
|
||||||
|
func (api *InputSimulatorAPI) validateRequest(req InputRequest) error {
|
||||||
|
// 使用配置中的默认值填充请求参数
|
||||||
|
if req.MinSegmentLen <= 0 {
|
||||||
|
req.MinSegmentLen = api.config.MinSegmentLen
|
||||||
|
}
|
||||||
|
if req.MaxSegmentLen <= 0 {
|
||||||
|
req.MaxSegmentLen = api.config.MaxSegmentLen
|
||||||
|
}
|
||||||
|
if req.MinDelayMs < 0 {
|
||||||
|
req.MinDelayMs = api.config.MinDelayMs
|
||||||
|
}
|
||||||
|
if req.MaxDelayMs < 0 {
|
||||||
|
req.MaxDelayMs = api.config.MaxDelayMs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitTextIntelligently 智能分割文本
|
||||||
|
// 规则:
|
||||||
|
// 1. 先分解成基础单元:中文每个字符一个单元,英文/数字连续的作为一个单元,其他字符各自一个单元
|
||||||
|
// 2. 按MinSegmentLen到MaxSegmentLen的随机值组合基础单元
|
||||||
|
func (api *InputSimulatorAPI) splitTextIntelligently(text string, minLen, maxLen int) []string {
|
||||||
|
if minLen <= 0 {
|
||||||
|
minLen = api.config.MinSegmentLen
|
||||||
|
}
|
||||||
|
if maxLen <= 0 {
|
||||||
|
maxLen = api.config.MaxSegmentLen
|
||||||
|
}
|
||||||
|
if maxLen < minLen {
|
||||||
|
maxLen = minLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一步:分解成基础单元
|
||||||
|
baseUnits := api.splitIntoBaseUnits(text)
|
||||||
|
|
||||||
|
// 第二步:按随机数组合基础单元
|
||||||
|
var segments []string
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
for i < len(baseUnits) {
|
||||||
|
remainingUnits := len(baseUnits) - i
|
||||||
|
|
||||||
|
var unitCount int
|
||||||
|
// 如果剩余单元数少于minLen,就把剩余的全部作为一个片段
|
||||||
|
if remainingUnits < minLen {
|
||||||
|
unitCount = remainingUnits
|
||||||
|
} else {
|
||||||
|
// 随机决定本次要组合的单元数量(在minLen到maxLen之间)
|
||||||
|
unitCount = minLen
|
||||||
|
if maxLen > minLen {
|
||||||
|
// 确保unitCount不超过剩余单元数
|
||||||
|
maxPossibleCount := maxLen
|
||||||
|
if maxPossibleCount > remainingUnits {
|
||||||
|
maxPossibleCount = remainingUnits
|
||||||
|
}
|
||||||
|
unitCount = minLen + api.rand.Intn(maxPossibleCount-minLen+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合unitCount个基础单元成一个片段
|
||||||
|
segment := ""
|
||||||
|
for j := 0; j < unitCount; j++ {
|
||||||
|
segment += baseUnits[i+j]
|
||||||
|
}
|
||||||
|
segments = append(segments, segment)
|
||||||
|
i += unitCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitIntoBaseUnits 将文本分解成基础单元
|
||||||
|
func (api *InputSimulatorAPI) splitIntoBaseUnits(text string) []string {
|
||||||
|
var units []string
|
||||||
|
runes := []rune(text)
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
for i < len(runes) {
|
||||||
|
// 处理中文字符:每个字符一个单元
|
||||||
|
if api.isChinese(runes[i]) {
|
||||||
|
units = append(units, string(runes[i]))
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理连续英文字母:作为一个单元
|
||||||
|
if unicode.IsLetter(runes[i]) && runes[i] <= 127 {
|
||||||
|
start := i
|
||||||
|
for i < len(runes) && unicode.IsLetter(runes[i]) && runes[i] <= 127 {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
word := string(runes[start:i])
|
||||||
|
units = append(units, word)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理连续数字:作为一个单元
|
||||||
|
if unicode.IsDigit(runes[i]) {
|
||||||
|
start := i
|
||||||
|
for i < len(runes) && unicode.IsDigit(runes[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
number := string(runes[start:i])
|
||||||
|
units = append(units, number)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理其他字符(空格、标点等):每个字符一个单元
|
||||||
|
units = append(units, string(runes[i]))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return units
|
||||||
|
}
|
||||||
|
|
||||||
|
// isChinese 判断字符是否为中文
|
||||||
|
func (api *InputSimulatorAPI) isChinese(r rune) bool {
|
||||||
|
return unicode.Is(unicode.Scripts["Han"], r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandomDelay 生成随机延迟时间
|
||||||
|
func (api *InputSimulatorAPI) generateRandomDelay(minDelayMs, maxDelayMs int) int {
|
||||||
|
if minDelayMs < 0 {
|
||||||
|
minDelayMs = api.config.MinDelayMs
|
||||||
|
}
|
||||||
|
if maxDelayMs < 0 {
|
||||||
|
maxDelayMs = api.config.MaxDelayMs
|
||||||
|
}
|
||||||
|
if maxDelayMs < minDelayMs {
|
||||||
|
maxDelayMs = minDelayMs
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxDelayMs == minDelayMs {
|
||||||
|
return minDelayMs
|
||||||
|
}
|
||||||
|
|
||||||
|
return minDelayMs + api.rand.Intn(maxDelayMs-minDelayMs+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitText 公开的文本分割函数(使用智能分割)
|
||||||
|
func (api *InputSimulatorAPI) SplitText(text string) []string {
|
||||||
|
return api.splitTextIntelligently(text, api.config.MinSegmentLen, api.config.MaxSegmentLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateDelay 公开的延迟生成函数
|
||||||
|
func (api *InputSimulatorAPI) GenerateDelay() int {
|
||||||
|
return api.generateRandomDelay(api.config.MinDelayMs, api.config.MaxDelayMs)
|
||||||
|
}
|
||||||
@@ -0,0 +1,956 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SlideRequest 滑动请求参数
|
||||||
|
type SlideRequest struct {
|
||||||
|
StartX float64 `json:"start_x"` // 起始X坐标
|
||||||
|
StartY float64 `json:"start_y"` // 起始Y坐标
|
||||||
|
Direction Direction `json:"direction"` // 滑动方向
|
||||||
|
Distance float64 `json:"distance"` // 滑动距离
|
||||||
|
DeviceID int `json:"device_id"` // 设备ID
|
||||||
|
Pressure float64 `json:"pressure"` // 压力值
|
||||||
|
Size float64 `json:"size"` // 按压大小(接触面积)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointToPointSlideRequest 点对点滑动请求参数
|
||||||
|
type PointToPointSlideRequest struct {
|
||||||
|
StartX float64 `json:"start_x"` // 起始X坐标
|
||||||
|
StartY float64 `json:"start_y"` // 起始Y坐标
|
||||||
|
EndX float64 `json:"end_x"` // 结束X坐标
|
||||||
|
EndY float64 `json:"end_y"` // 结束Y坐标
|
||||||
|
DeviceID int `json:"device_id"` // 设备ID
|
||||||
|
Pressure float64 `json:"pressure"` // 压力值
|
||||||
|
Size float64 `json:"size"` // 按压大小(接触面积)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlideResponse 滑动响应结果
|
||||||
|
type SlideResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Points []SlidePoint `json:"points"`
|
||||||
|
Metrics SlideMetrics `json:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlideMetrics 滑动指标
|
||||||
|
type SlideMetrics struct {
|
||||||
|
TotalDuration int64 `json:"total_duration_ms"` // 总持续时间(毫秒)
|
||||||
|
PointCount int `json:"point_count"` // 轨迹点数量
|
||||||
|
ActualDistance float64 `json:"actual_distance"` // 实际滑动距离
|
||||||
|
AverageInterval float64 `json:"average_interval_ms"` // 平均采样间隔
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlidePoint 滑动轨迹点
|
||||||
|
type SlidePoint struct {
|
||||||
|
Timestamp int64 `json:"timestamp"` // 时间戳(毫秒)
|
||||||
|
X float64 `json:"x"` // X坐标
|
||||||
|
Y float64 `json:"y"` // Y坐标
|
||||||
|
DeviceID int `json:"device_id"` // 设备ID
|
||||||
|
Pressure float64 `json:"pressure"` // 压力值
|
||||||
|
Size float64 `json:"size"` // 按压大小(接触面积)
|
||||||
|
EventTime int64 `json:"event_time"` // 相对第一个点的时间(ms),第一个点为0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direction 滑动方向枚举
|
||||||
|
type Direction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Up Direction = "up"
|
||||||
|
Down Direction = "down"
|
||||||
|
Left Direction = "left"
|
||||||
|
Right Direction = "right"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SlideConfig 滑动配置参数
|
||||||
|
type SlideConfig struct {
|
||||||
|
MinDuration int64 // 最小持续时间(毫秒)
|
||||||
|
MaxDuration int64 // 最大持续时间(毫秒)
|
||||||
|
MinPoints int // 最小点数
|
||||||
|
MaxPoints int // 最大点数
|
||||||
|
CurveIntensity float64 // 曲线强度(0-1)
|
||||||
|
NoiseLevel float64 // 噪声级别
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSlideConfig 默认配置
|
||||||
|
var DefaultSlideConfig = SlideConfig{
|
||||||
|
MinDuration: 80,
|
||||||
|
MaxDuration: 200,
|
||||||
|
MinPoints: 4,
|
||||||
|
MaxPoints: 8,
|
||||||
|
CurveIntensity: 0.05,
|
||||||
|
NoiseLevel: 2.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlideSimulatorAPI 滑动仿真API
|
||||||
|
type SlideSimulatorAPI struct {
|
||||||
|
rand *rand.Rand
|
||||||
|
config SlideConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCase 测试用例
|
||||||
|
type TestCase struct {
|
||||||
|
Name string
|
||||||
|
StartX float64
|
||||||
|
StartY float64
|
||||||
|
Direction Direction
|
||||||
|
Distance float64
|
||||||
|
DeviceID int
|
||||||
|
Pressure float64
|
||||||
|
Size float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSlideSimulatorAPI 创建新的滑动仿真API
|
||||||
|
func NewSlideSimulatorAPI(config *SlideConfig) *SlideSimulatorAPI {
|
||||||
|
if config == nil {
|
||||||
|
config = &DefaultSlideConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SlideSimulatorAPI{
|
||||||
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
config: *config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSlide 生成滑动轨迹
|
||||||
|
func (api *SlideSimulatorAPI) GenerateSlide(req SlideRequest) SlideResponse {
|
||||||
|
// 验证输入参数
|
||||||
|
if err := api.validateRequest(req); err != nil {
|
||||||
|
return SlideResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成滑动轨迹
|
||||||
|
points := api.generateSlidePoints(req)
|
||||||
|
|
||||||
|
// 计算指标
|
||||||
|
metrics := api.calculateMetrics(points)
|
||||||
|
|
||||||
|
return SlideResponse{
|
||||||
|
Success: true,
|
||||||
|
Points: points,
|
||||||
|
Metrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pressureRiseCurve 压力上升曲线,模拟真实的压力变化
|
||||||
|
func (api *SlideSimulatorAPI) pressureRiseCurve(t float64) float64 {
|
||||||
|
// 使用二次函数模拟压力逐渐增加的过程
|
||||||
|
return t*t*0.6 + t*0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
// pressureFallCurve 压力下降曲线,模拟真实的压力变化
|
||||||
|
func (api *SlideSimulatorAPI) pressureFallCurve(t float64) float64 {
|
||||||
|
// 使用指数衰减模拟压力快速下降的过程
|
||||||
|
return 1.0 - (1.0-math.Exp(-t*2.0))*0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePointToPointSlide 生成点对点滑动轨迹
|
||||||
|
func (api *SlideSimulatorAPI) GeneratePointToPointSlide(req PointToPointSlideRequest) SlideResponse {
|
||||||
|
// 验证输入参数
|
||||||
|
if err := api.validatePointToPointRequest(req); err != nil {
|
||||||
|
return SlideResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成滑动轨迹
|
||||||
|
points := api.generatePointToPointSlidePoints(req)
|
||||||
|
|
||||||
|
// 计算指标
|
||||||
|
metrics := api.calculateMetrics(points)
|
||||||
|
|
||||||
|
return SlideResponse{
|
||||||
|
Success: true,
|
||||||
|
Points: points,
|
||||||
|
Metrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateRequest 验证请求参数
|
||||||
|
func (api *SlideSimulatorAPI) validateRequest(req SlideRequest) error {
|
||||||
|
if req.Distance <= 0 {
|
||||||
|
return fmt.Errorf("distance must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Direction {
|
||||||
|
case Up, Down, Left, Right:
|
||||||
|
// 有效方向
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid direction: %s", req.Direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePointToPointRequest 验证点对点请求参数
|
||||||
|
func (api *SlideSimulatorAPI) validatePointToPointRequest(req PointToPointSlideRequest) error {
|
||||||
|
// 检查起始点和结束点是否相同
|
||||||
|
if req.StartX == req.EndX && req.StartY == req.EndY {
|
||||||
|
return fmt.Errorf("start point and end point cannot be the same")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查距离是否合理
|
||||||
|
distance := math.Sqrt((req.EndX-req.StartX)*(req.EndX-req.StartX) + (req.EndY-req.StartY)*(req.EndY-req.StartY))
|
||||||
|
if distance < 10 {
|
||||||
|
return fmt.Errorf("distance too short: %.2f pixels", distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSlidePoints 生成滑动轨迹点
|
||||||
|
func (api *SlideSimulatorAPI) generateSlidePoints(req SlideRequest) []SlidePoint {
|
||||||
|
// 计算终点坐标
|
||||||
|
endX, endY := api.calculateEndPoint(req.StartX, req.StartY, req.Direction, req.Distance)
|
||||||
|
|
||||||
|
// 计算滑动参数
|
||||||
|
duration := api.calculateDuration(req.Distance)
|
||||||
|
pointCount := api.calculatePointCount(duration)
|
||||||
|
|
||||||
|
// 生成时间戳序列
|
||||||
|
timestamps := api.generateTimestamps(duration, pointCount)
|
||||||
|
|
||||||
|
// 生成轨迹点
|
||||||
|
points := make([]SlidePoint, pointCount)
|
||||||
|
|
||||||
|
// 计算总偏移趋势(基于真实数据分析)
|
||||||
|
var totalOffsetX, totalOffsetY float64
|
||||||
|
switch req.Direction {
|
||||||
|
case Up:
|
||||||
|
// 上滑时倾向于向右偏移,偏移量为距离的15%-35%
|
||||||
|
offsetRatio := 0.15 + api.rand.Float64()*0.20
|
||||||
|
totalOffsetX = req.Distance * offsetRatio
|
||||||
|
totalOffsetY = 0
|
||||||
|
case Down:
|
||||||
|
// 下滑时可以左右偏移,但偏移较小
|
||||||
|
offsetRatio := 0.10 + api.rand.Float64()*0.15
|
||||||
|
totalOffsetX = (api.rand.Float64() - 0.5) * req.Distance * offsetRatio
|
||||||
|
totalOffsetY = 0
|
||||||
|
case Left:
|
||||||
|
// 左滑时可能向上或向下偏移
|
||||||
|
offsetRatio := 0.05 + api.rand.Float64()*0.20
|
||||||
|
totalOffsetX = 0
|
||||||
|
totalOffsetY = (api.rand.Float64() - 0.5) * req.Distance * offsetRatio
|
||||||
|
case Right:
|
||||||
|
// 右滑时偏移相对较小
|
||||||
|
offsetRatio := 0.03 + api.rand.Float64()*0.10
|
||||||
|
totalOffsetX = 0
|
||||||
|
totalOffsetY = (api.rand.Float64() - 0.5) * req.Distance * offsetRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成size变化曲线(基于真实数据分析)
|
||||||
|
sizeValues := api.generateSizeValues(pointCount, req.Size)
|
||||||
|
|
||||||
|
// 生成pressure变化曲线(基于真实数据分析)
|
||||||
|
pressureValues := api.generatePressureValues(pointCount, req.Pressure, req.Direction)
|
||||||
|
|
||||||
|
baseTimestamp := timestamps[0]
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
progress := float64(i) / float64(pointCount-1)
|
||||||
|
|
||||||
|
// 使用贝塞尔曲线生成基础轨迹
|
||||||
|
x, y := api.calculateBezierPoint(req.StartX, req.StartY, endX, endY, progress, req.Direction)
|
||||||
|
|
||||||
|
// 添加渐进式偏移(模拟真实滑动的累积偏移)
|
||||||
|
progressiveOffsetX := totalOffsetX * api.getProgressiveOffset(progress)
|
||||||
|
progressiveOffsetY := totalOffsetY * api.getProgressiveOffset(progress)
|
||||||
|
|
||||||
|
x += progressiveOffsetX
|
||||||
|
y += progressiveOffsetY
|
||||||
|
|
||||||
|
// 添加随机噪声(减小噪声强度,因为主要偏移已经通过渐进式偏移实现)
|
||||||
|
x += api.addNoise(api.config.NoiseLevel * 0.5)
|
||||||
|
y += api.addNoise(api.config.NoiseLevel * 0.5)
|
||||||
|
|
||||||
|
eventTime := timestamps[i] - baseTimestamp
|
||||||
|
|
||||||
|
points[i] = SlidePoint{
|
||||||
|
Timestamp: timestamps[i],
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
DeviceID: req.DeviceID,
|
||||||
|
Pressure: pressureValues[i],
|
||||||
|
Size: sizeValues[i],
|
||||||
|
EventTime: eventTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatePointToPointSlidePoints 生成点对点滑动轨迹点
|
||||||
|
func (api *SlideSimulatorAPI) generatePointToPointSlidePoints(req PointToPointSlideRequest) []SlidePoint {
|
||||||
|
// 对起始点和结束点添加随机偏移(正负20以内)
|
||||||
|
offsetRange := 20.0
|
||||||
|
|
||||||
|
actualStartX := req.StartX + api.addNoise(offsetRange)
|
||||||
|
actualStartY := req.StartY + api.addNoise(offsetRange)
|
||||||
|
actualEndX := req.EndX + api.addNoise(offsetRange)
|
||||||
|
actualEndY := req.EndY + api.addNoise(offsetRange)
|
||||||
|
|
||||||
|
// 计算实际距离
|
||||||
|
distance := math.Sqrt((actualEndX-actualStartX)*(actualEndX-actualStartX) + (actualEndY-actualStartY)*(actualEndY-actualStartY))
|
||||||
|
|
||||||
|
// 计算滑动参数
|
||||||
|
duration := api.calculateDuration(distance)
|
||||||
|
pointCount := api.calculatePointCount(duration)
|
||||||
|
|
||||||
|
// 生成时间戳序列
|
||||||
|
timestamps := api.generateTimestamps(duration, pointCount)
|
||||||
|
|
||||||
|
// 生成轨迹点
|
||||||
|
points := make([]SlidePoint, pointCount)
|
||||||
|
|
||||||
|
// 判断主要滑动方向,用于计算偏移
|
||||||
|
dx := actualEndX - actualStartX
|
||||||
|
dy := actualEndY - actualStartY
|
||||||
|
var direction Direction
|
||||||
|
if math.Abs(dy) > math.Abs(dx) {
|
||||||
|
if dy < 0 {
|
||||||
|
direction = Up
|
||||||
|
} else {
|
||||||
|
direction = Down
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dx < 0 {
|
||||||
|
direction = Left
|
||||||
|
} else {
|
||||||
|
direction = Right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总偏移趋势(基于主要方向)
|
||||||
|
var totalOffsetX, totalOffsetY float64
|
||||||
|
switch direction {
|
||||||
|
case Up:
|
||||||
|
// 上滑时倾向于向右偏移
|
||||||
|
offsetRatio := 0.10 + api.rand.Float64()*0.15
|
||||||
|
totalOffsetX = distance * offsetRatio
|
||||||
|
totalOffsetY = 0
|
||||||
|
case Down:
|
||||||
|
// 下滑时可以左右偏移,但偏移较小
|
||||||
|
offsetRatio := 0.05 + api.rand.Float64()*0.10
|
||||||
|
totalOffsetX = (api.rand.Float64() - 0.5) * distance * offsetRatio
|
||||||
|
totalOffsetY = 0
|
||||||
|
case Left:
|
||||||
|
// 左滑时可能向上或向下偏移
|
||||||
|
offsetRatio := 0.03 + api.rand.Float64()*0.15
|
||||||
|
totalOffsetX = 0
|
||||||
|
totalOffsetY = (api.rand.Float64() - 0.5) * distance * offsetRatio
|
||||||
|
case Right:
|
||||||
|
// 右滑时偏移相对较小
|
||||||
|
offsetRatio := 0.02 + api.rand.Float64()*0.08
|
||||||
|
totalOffsetX = 0
|
||||||
|
totalOffsetY = (api.rand.Float64() - 0.5) * distance * offsetRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成size变化曲线
|
||||||
|
sizeValues := api.generateSizeValues(pointCount, req.Size)
|
||||||
|
|
||||||
|
// 生成pressure变化曲线
|
||||||
|
pressureValues := api.generatePressureValues(pointCount, req.Pressure, direction)
|
||||||
|
|
||||||
|
baseTimestamp := timestamps[0]
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
progress := float64(i) / float64(pointCount-1)
|
||||||
|
|
||||||
|
// 使用贝塞尔曲线生成基础轨迹
|
||||||
|
x, y := api.calculateBezierPoint(actualStartX, actualStartY, actualEndX, actualEndY, progress, direction)
|
||||||
|
|
||||||
|
// 添加渐进式偏移
|
||||||
|
progressiveOffsetX := totalOffsetX * api.getProgressiveOffset(progress)
|
||||||
|
progressiveOffsetY := totalOffsetY * api.getProgressiveOffset(progress)
|
||||||
|
|
||||||
|
x += progressiveOffsetX
|
||||||
|
y += progressiveOffsetY
|
||||||
|
|
||||||
|
// 添加随机噪声
|
||||||
|
x += api.addNoise(api.config.NoiseLevel * 0.5)
|
||||||
|
y += api.addNoise(api.config.NoiseLevel * 0.5)
|
||||||
|
|
||||||
|
eventTime := timestamps[i] - baseTimestamp
|
||||||
|
|
||||||
|
points[i] = SlidePoint{
|
||||||
|
Timestamp: timestamps[i],
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
DeviceID: req.DeviceID,
|
||||||
|
Pressure: pressureValues[i],
|
||||||
|
Size: sizeValues[i],
|
||||||
|
EventTime: eventTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSizeValues 生成size值序列,基于真实数据分析
|
||||||
|
func (api *SlideSimulatorAPI) generateSizeValues(pointCount int, baseSize float64) []float64 {
|
||||||
|
sizes := make([]float64, pointCount)
|
||||||
|
|
||||||
|
// 如果baseSize为0,使用默认值
|
||||||
|
if baseSize == 0 {
|
||||||
|
baseSize = 0.04 // 默认size值,基于真实数据平均值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态计算size范围,基于baseSize的值来适应不同设备
|
||||||
|
var minSize, maxSize float64
|
||||||
|
if baseSize < 1.0 {
|
||||||
|
// 小数值范围(如0.04),使用原有逻辑
|
||||||
|
minSize = 0.031
|
||||||
|
maxSize = 0.063
|
||||||
|
// 确保baseSize在合理范围内
|
||||||
|
if baseSize < minSize {
|
||||||
|
baseSize = minSize + api.rand.Float64()*(maxSize-minSize)*0.3
|
||||||
|
}
|
||||||
|
if baseSize > maxSize {
|
||||||
|
baseSize = maxSize - api.rand.Float64()*(maxSize-minSize)*0.3
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 大数值范围(如几十或几百),基于baseSize动态计算范围
|
||||||
|
// 允许在baseSize的±20%范围内变化
|
||||||
|
minSize = baseSize * 0.8
|
||||||
|
maxSize = baseSize * 1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
// 基础size值随滑动进度变化
|
||||||
|
var sizeModifier float64
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
// 开始时:可能较大或较小,有随机性
|
||||||
|
sizeModifier = 0.8 + api.rand.Float64()*0.4 // 0.8-1.2倍
|
||||||
|
} else if i == pointCount-1 {
|
||||||
|
// 结束时:可能增大(手指离开前压力增加)
|
||||||
|
if api.rand.Float64() < 0.6 { // 60%概率增大
|
||||||
|
sizeModifier = 1.1 + api.rand.Float64()*0.3 // 1.1-1.4倍
|
||||||
|
} else {
|
||||||
|
sizeModifier = 0.9 + api.rand.Float64()*0.2 // 0.9-1.1倍
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 中间过程:轻微波动
|
||||||
|
sizeModifier = 0.85 + api.rand.Float64()*0.3 // 0.85-1.15倍
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用变化
|
||||||
|
sizes[i] = baseSize * sizeModifier
|
||||||
|
|
||||||
|
// 确保在合理范围内
|
||||||
|
if sizes[i] < minSize {
|
||||||
|
sizes[i] = minSize
|
||||||
|
}
|
||||||
|
if sizes[i] > maxSize {
|
||||||
|
sizes[i] = maxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加轻微随机噪声,噪声大小根据baseSize动态调整
|
||||||
|
var noiseLevel float64
|
||||||
|
if baseSize < 1.0 {
|
||||||
|
noiseLevel = 0.003 // 小数值使用固定的小噪声
|
||||||
|
} else {
|
||||||
|
noiseLevel = baseSize * 0.01 // 大数值使用baseSize的1%作为噪声
|
||||||
|
}
|
||||||
|
sizes[i] += api.addNoise(noiseLevel)
|
||||||
|
|
||||||
|
// 最终范围检查
|
||||||
|
if sizes[i] < minSize {
|
||||||
|
sizes[i] = minSize
|
||||||
|
}
|
||||||
|
if sizes[i] > maxSize {
|
||||||
|
sizes[i] = maxSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatePressureValues 生成pressure值序列,基于用户输入的压力值动态仿真
|
||||||
|
func (api *SlideSimulatorAPI) generatePressureValues(pointCount int, basePressure float64, direction Direction) []float64 {
|
||||||
|
pressures := make([]float64, pointCount)
|
||||||
|
|
||||||
|
// 如果用户没有提供压力值,使用默认值
|
||||||
|
if basePressure <= 0 {
|
||||||
|
basePressure = 1 // 默认压力值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特殊处理:当压力值为1时,保持恒定不变
|
||||||
|
if basePressure == 1 {
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
pressures[i] = 1.0
|
||||||
|
}
|
||||||
|
return pressures
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将整数压力值转换为浮点数
|
||||||
|
baseP := float64(basePressure)
|
||||||
|
|
||||||
|
// 基于真实数据观察的压力变化规律:
|
||||||
|
// 1. 起始压力:基础压力的70%-90%
|
||||||
|
// 2. 峰值压力:基础压力的120%-180%
|
||||||
|
// 3. 结束压力:基础压力的30%-60%
|
||||||
|
|
||||||
|
startPressureRatio := 0.7 + api.rand.Float64()*0.2 // 70%-90%
|
||||||
|
peakPressureRatio := 1.2 + api.rand.Float64()*0.6 // 120%-180%
|
||||||
|
endPressureRatio := 0.3 + api.rand.Float64()*0.3 // 30%-60%
|
||||||
|
|
||||||
|
startPressure := baseP * startPressureRatio
|
||||||
|
peakPressure := baseP * peakPressureRatio
|
||||||
|
endPressure := baseP * endPressureRatio
|
||||||
|
|
||||||
|
// 峰值出现的位置:通常在滑动过程的20%-70%处
|
||||||
|
peakPosition := 0.2 + api.rand.Float64()*0.5
|
||||||
|
peakIndex := int(float64(pointCount-1) * peakPosition)
|
||||||
|
if peakIndex >= pointCount {
|
||||||
|
peakIndex = pointCount - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保压力值在合理范围内(0.5-15.0)
|
||||||
|
//if startPressure < 0.5 {
|
||||||
|
// startPressure = 0.5
|
||||||
|
//}
|
||||||
|
//if peakPressure > 15.0 {
|
||||||
|
// peakPressure = 15.0
|
||||||
|
//}
|
||||||
|
//if endPressure < 0.5 {
|
||||||
|
// endPressure = 0.5
|
||||||
|
//}
|
||||||
|
|
||||||
|
for i := 0; i < pointCount; i++ {
|
||||||
|
var pressure float64
|
||||||
|
|
||||||
|
if i <= peakIndex {
|
||||||
|
// 上升阶段:从起始到峰值
|
||||||
|
if peakIndex == 0 {
|
||||||
|
pressure = startPressure
|
||||||
|
} else {
|
||||||
|
t := float64(i) / float64(peakIndex)
|
||||||
|
// 使用非线性插值,模拟真实的压力上升曲线
|
||||||
|
t = api.pressureRiseCurve(t)
|
||||||
|
pressure = startPressure + (peakPressure-startPressure)*t
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 下降阶段:从峰值到结束
|
||||||
|
t := float64(i-peakIndex) / float64(pointCount-1-peakIndex)
|
||||||
|
// 使用非线性插值,模拟真实的压力下降曲线
|
||||||
|
t = api.pressureFallCurve(t)
|
||||||
|
pressure = peakPressure + (endPressure-peakPressure)*t
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加随机噪声(±8%),模拟真实手指压力的微小波动
|
||||||
|
noiseRange := pressure * 0.08
|
||||||
|
noise := (api.rand.Float64() - 0.5) * noiseRange
|
||||||
|
pressure += noise
|
||||||
|
|
||||||
|
// 确保pressure在合理范围内
|
||||||
|
//if pressure < 0.5 {
|
||||||
|
// pressure = 0.5 + api.rand.Float64()*0.3
|
||||||
|
//}
|
||||||
|
//if pressure > 15.0 {
|
||||||
|
// pressure = 14.5 + api.rand.Float64()*0.5
|
||||||
|
//}
|
||||||
|
|
||||||
|
// 保留两位小数精度
|
||||||
|
pressures[i] = math.Round(pressure*100) / 100
|
||||||
|
|
||||||
|
// 对于最后一个点,可能会有重复(基于真实数据观察)
|
||||||
|
if i == pointCount-1 && api.rand.Float64() < 0.25 {
|
||||||
|
// 25%概率最后一个点重复前一个点的压力值
|
||||||
|
if i > 0 {
|
||||||
|
pressures[i] = pressures[i-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pressures
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProgressiveOffset 获取渐进式偏移系数
|
||||||
|
func (api *SlideSimulatorAPI) getProgressiveOffset(progress float64) float64 {
|
||||||
|
// 使用二次函数让偏移逐渐增加,模拟真实滑动中的累积偏移
|
||||||
|
// 开始时偏移较小,中后期偏移逐渐增大
|
||||||
|
return progress*progress*0.7 + progress*0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateEndPoint 计算终点坐标
|
||||||
|
func (api *SlideSimulatorAPI) calculateEndPoint(startX, startY float64, direction Direction, distance float64) (float64, float64) {
|
||||||
|
switch direction {
|
||||||
|
case Up:
|
||||||
|
return startX, startY - distance
|
||||||
|
case Down:
|
||||||
|
return startX, startY + distance
|
||||||
|
case Left:
|
||||||
|
return startX - distance, startY
|
||||||
|
case Right:
|
||||||
|
return startX + distance, startY
|
||||||
|
default:
|
||||||
|
return startX, startY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateDuration 计算滑动持续时间
|
||||||
|
func (api *SlideSimulatorAPI) calculateDuration(distance float64) int64 {
|
||||||
|
// 基于真实数据的持续时间算法
|
||||||
|
baseDuration := 120.0
|
||||||
|
variableDuration := distance * 0.05
|
||||||
|
randomFactor := api.rand.Float64()*40 - 20
|
||||||
|
|
||||||
|
duration := baseDuration + variableDuration + randomFactor
|
||||||
|
|
||||||
|
if duration < float64(api.config.MinDuration) {
|
||||||
|
duration = float64(api.config.MinDuration)
|
||||||
|
}
|
||||||
|
if duration > float64(api.config.MaxDuration) {
|
||||||
|
duration = float64(api.config.MaxDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculatePointCount 计算轨迹点数量
|
||||||
|
func (api *SlideSimulatorAPI) calculatePointCount(duration int64) int {
|
||||||
|
avgInterval := 20.0 + api.rand.Float64()*10
|
||||||
|
count := int(float64(duration)/avgInterval) + 1
|
||||||
|
|
||||||
|
if count < api.config.MinPoints {
|
||||||
|
count = api.config.MinPoints
|
||||||
|
}
|
||||||
|
if count > api.config.MaxPoints {
|
||||||
|
count = api.config.MaxPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateTimestamps 生成时间戳序列
|
||||||
|
func (api *SlideSimulatorAPI) generateTimestamps(duration int64, pointCount int) []int64 {
|
||||||
|
baseTime := time.Now().UnixMilli()
|
||||||
|
timestamps := make([]int64, pointCount)
|
||||||
|
|
||||||
|
timestamps[0] = baseTime
|
||||||
|
|
||||||
|
for i := 1; i < pointCount; i++ {
|
||||||
|
progress := float64(i) / float64(pointCount-1)
|
||||||
|
timeProgress := api.speedCurve(progress)
|
||||||
|
timestamps[i] = baseTime + int64(timeProgress*float64(duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
return timestamps
|
||||||
|
}
|
||||||
|
|
||||||
|
// speedCurve 速度曲线函数
|
||||||
|
func (api *SlideSimulatorAPI) speedCurve(progress float64) float64 {
|
||||||
|
// 模拟真实滑动的速度变化
|
||||||
|
if progress <= 0.5 {
|
||||||
|
return 0.8*progress*progress + 0.2*progress
|
||||||
|
} else {
|
||||||
|
return 0.2 + 0.8*(2*progress-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateBezierPoint 计算贝塞尔曲线点
|
||||||
|
func (api *SlideSimulatorAPI) calculateBezierPoint(startX, startY, endX, endY, progress float64, direction Direction) (float64, float64) {
|
||||||
|
controlX, controlY := api.calculateControlPoint(startX, startY, endX, endY, direction)
|
||||||
|
|
||||||
|
t := progress
|
||||||
|
oneMinusT := 1 - t
|
||||||
|
|
||||||
|
x := oneMinusT*oneMinusT*startX + 2*oneMinusT*t*controlX + t*t*endX
|
||||||
|
y := oneMinusT*oneMinusT*startY + 2*oneMinusT*t*controlY + t*t*endY
|
||||||
|
|
||||||
|
return x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateControlPoint 计算控制点
|
||||||
|
func (api *SlideSimulatorAPI) calculateControlPoint(startX, startY, endX, endY float64, direction Direction) (float64, float64) {
|
||||||
|
midX := (startX + endX) / 2
|
||||||
|
midY := (startY + endY) / 2
|
||||||
|
|
||||||
|
distance := math.Sqrt((endX-startX)*(endX-startX) + (endY-startY)*(endY-startY))
|
||||||
|
|
||||||
|
var offsetX, offsetY float64
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case Up, Down:
|
||||||
|
// 垂直滑动时的X轴偏移:根据真实数据分析,平均偏移比例为25.8%
|
||||||
|
// 偏移范围:距离的15%-35%
|
||||||
|
offsetRatio := 0.15 + api.rand.Float64()*0.20 // 15%-35%
|
||||||
|
maxOffsetX := distance * offsetRatio
|
||||||
|
|
||||||
|
// 上滑时倾向于向右偏移,下滑时可以任意方向
|
||||||
|
if direction == Up {
|
||||||
|
offsetX = api.rand.Float64() * maxOffsetX // 0到最大偏移(向右)
|
||||||
|
} else {
|
||||||
|
offsetX = (api.rand.Float64() - 0.5) * maxOffsetX // 左右偏移
|
||||||
|
}
|
||||||
|
offsetY = 0
|
||||||
|
|
||||||
|
case Left, Right:
|
||||||
|
// 水平滑动时的Y轴偏移:根据真实数据分析,平均偏移比例为12.5%
|
||||||
|
// 偏移范围:距离的5%-25%
|
||||||
|
offsetRatio := 0.05 + api.rand.Float64()*0.20 // 5%-25%
|
||||||
|
maxOffsetY := distance * offsetRatio
|
||||||
|
|
||||||
|
offsetX = 0
|
||||||
|
// 左滑时可能向上或向下偏移,右滑时偏移较小
|
||||||
|
if direction == Left {
|
||||||
|
offsetY = (api.rand.Float64() - 0.5) * maxOffsetY
|
||||||
|
} else {
|
||||||
|
// 右滑时偏移相对较小
|
||||||
|
offsetY = (api.rand.Float64() - 0.5) * maxOffsetY * 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return midX + offsetX, midY + offsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNoise 添加随机噪声
|
||||||
|
func (api *SlideSimulatorAPI) addNoise(maxNoise float64) float64 {
|
||||||
|
return (api.rand.Float64() - 0.5) * maxNoise
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateMetrics 计算滑动指标
|
||||||
|
func (api *SlideSimulatorAPI) calculateMetrics(points []SlidePoint) SlideMetrics {
|
||||||
|
if len(points) == 0 {
|
||||||
|
return SlideMetrics{}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDuration := points[len(points)-1].Timestamp - points[0].Timestamp
|
||||||
|
|
||||||
|
// 计算实际距离
|
||||||
|
var actualDistance float64
|
||||||
|
for i := 1; i < len(points); i++ {
|
||||||
|
dx := points[i].X - points[i-1].X
|
||||||
|
dy := points[i].Y - points[i-1].Y
|
||||||
|
actualDistance += math.Sqrt(dx*dx + dy*dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算平均间隔
|
||||||
|
var averageInterval float64
|
||||||
|
if len(points) > 1 {
|
||||||
|
averageInterval = float64(totalDuration) / float64(len(points)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SlideMetrics{
|
||||||
|
TotalDuration: totalDuration,
|
||||||
|
PointCount: len(points),
|
||||||
|
ActualDistance: actualDistance,
|
||||||
|
AverageInterval: averageInterval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON 将结果转换为JSON
|
||||||
|
func (resp SlideResponse) ToJSON() (string, error) {
|
||||||
|
data, err := json.MarshalIndent(resp, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToTouchEvents 将SlidePoint切片转换为TouchEvent切片
|
||||||
|
func (api *SlideSimulatorAPI) ConvertToTouchEvents(points []SlidePoint) []types.TouchEvent {
|
||||||
|
if len(points) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
events := make([]types.TouchEvent, len(points))
|
||||||
|
baseDownTime := points[0].Timestamp
|
||||||
|
|
||||||
|
for i, point := range points {
|
||||||
|
var action int
|
||||||
|
if i == 0 {
|
||||||
|
action = 0 // ACTION_DOWN
|
||||||
|
} else if i == len(points)-1 {
|
||||||
|
action = 1 // ACTION_UP
|
||||||
|
} else {
|
||||||
|
action = 2 // ACTION_MOVE
|
||||||
|
}
|
||||||
|
|
||||||
|
events[i] = types.TouchEvent{
|
||||||
|
X: point.X,
|
||||||
|
Y: point.Y,
|
||||||
|
DeviceID: point.DeviceID,
|
||||||
|
Pressure: float64(point.Pressure),
|
||||||
|
Size: point.Size,
|
||||||
|
RawX: point.X, // 使用相同的X坐标
|
||||||
|
RawY: point.Y, // 使用相同的Y坐标
|
||||||
|
DownTime: baseDownTime, // 第一个事件的时间戳作为DownTime
|
||||||
|
EventTime: point.Timestamp,
|
||||||
|
ToolType: 1, // TOOL_TYPE_FINGER
|
||||||
|
Flag: 0, // 默认flag
|
||||||
|
Action: action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSlideWithRandomDistance 生成指定方向和随机距离的滑动轨迹
|
||||||
|
func (api *SlideSimulatorAPI) GenerateSlideWithRandomDistance(startX, startY float64, direction Direction, minDistance, maxDistance float64, deviceID int, pressure float64, size float64) ([]types.TouchEvent, error) {
|
||||||
|
// 验证输入参数
|
||||||
|
if minDistance <= 0 || maxDistance < minDistance {
|
||||||
|
return nil, fmt.Errorf("invalid distance range: minDistance=%.2f, maxDistance=%.2f", minDistance, maxDistance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算实际滑动距离
|
||||||
|
var actualDistance float64
|
||||||
|
if minDistance == maxDistance {
|
||||||
|
actualDistance = minDistance
|
||||||
|
} else {
|
||||||
|
actualDistance = minDistance + api.rand.Float64()*(maxDistance-minDistance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建滑动请求
|
||||||
|
req := SlideRequest{
|
||||||
|
StartX: startX,
|
||||||
|
StartY: startY,
|
||||||
|
Direction: direction,
|
||||||
|
Distance: actualDistance,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
Pressure: pressure,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成滑动轨迹
|
||||||
|
response := api.GenerateSlide(req)
|
||||||
|
if !response.Success {
|
||||||
|
return nil, fmt.Errorf("generate slide failed: %s", response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为TouchEvent
|
||||||
|
events := api.ConvertToTouchEvents(response.Points)
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSlideInArea 在指定区域内生成滑动轨迹
|
||||||
|
func (api *SlideSimulatorAPI) GenerateSlideInArea(areaStartX, areaStartY, areaEndX, areaEndY float64, direction Direction, minDistance, maxDistance float64, deviceID int, pressure float64, size float64) ([]types.TouchEvent, error) {
|
||||||
|
// 验证输入参数
|
||||||
|
if minDistance <= 0 || maxDistance < minDistance {
|
||||||
|
return nil, fmt.Errorf("invalid distance range: minDistance=%.2f, maxDistance=%.2f", minDistance, maxDistance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证区域参数(允许start和end相等,表示单点区域)
|
||||||
|
if areaStartX > areaEndX || areaStartY > areaEndY {
|
||||||
|
return nil, fmt.Errorf("invalid area: start point (%.2f, %.2f) should be less than or equal to end point (%.2f, %.2f)",
|
||||||
|
areaStartX, areaStartY, areaEndX, areaEndY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在区域内随机选择起始点(如果start和end相等,则使用固定点)
|
||||||
|
var randomStartX, randomStartY float64
|
||||||
|
|
||||||
|
if areaStartX == areaEndX {
|
||||||
|
randomStartX = areaStartX // 单点X坐标
|
||||||
|
} else {
|
||||||
|
areaWidth := areaEndX - areaStartX
|
||||||
|
randomStartX = areaStartX + api.rand.Float64()*areaWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
if areaStartY == areaEndY {
|
||||||
|
randomStartY = areaStartY // 单点Y坐标
|
||||||
|
} else {
|
||||||
|
areaHeight := areaEndY - areaStartY
|
||||||
|
randomStartY = areaStartY + api.rand.Float64()*areaHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算实际滑动距离
|
||||||
|
var actualDistance float64
|
||||||
|
if minDistance == maxDistance {
|
||||||
|
actualDistance = minDistance
|
||||||
|
} else {
|
||||||
|
actualDistance = minDistance + api.rand.Float64()*(maxDistance-minDistance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证滑动后的点是否会超出屏幕边界(这里做简单检查)
|
||||||
|
// 可以根据实际需要调整边界检查逻辑
|
||||||
|
endX, endY := api.calculateEndPoint(randomStartX, randomStartY, direction, actualDistance)
|
||||||
|
|
||||||
|
// 如果滑动后超出合理范围,调整起始点位置
|
||||||
|
const marginBuffer = 50.0 // 边界缓冲区
|
||||||
|
switch direction {
|
||||||
|
case Up:
|
||||||
|
if endY < marginBuffer {
|
||||||
|
randomStartY = math.Min(areaEndY-marginBuffer, randomStartY+actualDistance)
|
||||||
|
}
|
||||||
|
case Down:
|
||||||
|
// 这里假设屏幕高度最大为2400,可以根据实际需要调整
|
||||||
|
if endY > 2400-marginBuffer {
|
||||||
|
randomStartY = math.Max(areaStartY+marginBuffer, randomStartY-actualDistance)
|
||||||
|
}
|
||||||
|
case Left:
|
||||||
|
if endX < marginBuffer {
|
||||||
|
randomStartX = math.Min(areaEndX-marginBuffer, randomStartX+actualDistance)
|
||||||
|
}
|
||||||
|
case Right:
|
||||||
|
// 这里假设屏幕宽度最大为1800,可以根据实际需要调整
|
||||||
|
if endX > 1800-marginBuffer {
|
||||||
|
randomStartX = math.Max(areaStartX+marginBuffer, randomStartX-actualDistance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建滑动请求
|
||||||
|
req := SlideRequest{
|
||||||
|
StartX: randomStartX,
|
||||||
|
StartY: randomStartY,
|
||||||
|
Direction: direction,
|
||||||
|
Distance: actualDistance,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
Pressure: pressure,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成滑动轨迹
|
||||||
|
response := api.GenerateSlide(req)
|
||||||
|
if !response.Success {
|
||||||
|
return nil, fmt.Errorf("generate slide failed: %s", response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为TouchEvent
|
||||||
|
events := api.ConvertToTouchEvents(response.Points)
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePointToPointSlideEvents 生成点对点滑动的TouchEvent序列
|
||||||
|
func (api *SlideSimulatorAPI) GeneratePointToPointSlideEvents(startX, startY, endX, endY float64, deviceID int, pressure float64, size float64) ([]types.TouchEvent, error) {
|
||||||
|
// 验证输入参数
|
||||||
|
if startX == endX && startY == endY {
|
||||||
|
return nil, fmt.Errorf("start point (%.2f, %.2f) and end point (%.2f, %.2f) cannot be the same", startX, startY, endX, endY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算距离
|
||||||
|
distance := math.Sqrt((endX-startX)*(endX-startX) + (endY-startY)*(endY-startY))
|
||||||
|
if distance < 10 {
|
||||||
|
return nil, fmt.Errorf("distance too short: %.2f pixels", distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建点对点滑动请求
|
||||||
|
req := PointToPointSlideRequest{
|
||||||
|
StartX: startX,
|
||||||
|
StartY: startY,
|
||||||
|
EndX: endX,
|
||||||
|
EndY: endY,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
Pressure: pressure,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成滑动轨迹
|
||||||
|
response := api.GeneratePointToPointSlide(req)
|
||||||
|
if !response.Success {
|
||||||
|
return nil, fmt.Errorf("generate point to point slide failed: %s", response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为TouchEvent
|
||||||
|
events := api.ConvertToTouchEvents(response.Points)
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
@@ -343,9 +343,8 @@ func (g *HTMLReportGenerator) encodeImageToBase64(imagePath string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and compress the image using the unified compression function
|
// Read and compress the image with quality 50
|
||||||
// Enable resize with max width 800px for HTML reports
|
compressedData, err := uixt.CompressImageFile(imagePath, 50)
|
||||||
compressedData, err := uixt.CompressImageFile(imagePath, true, 800)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("path", imagePath).Msg("failed to compress image, using original")
|
log.Warn().Err(err).Str("path", imagePath).Msg("failed to compress image, using original")
|
||||||
// Fallback to original image if compression fails
|
// Fallback to original image if compression fails
|
||||||
|
|||||||
+94
@@ -290,6 +290,100 @@ func (s *StepMobile) SwipeRight(opts ...option.ActionOption) *StepMobile {
|
|||||||
return s
|
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 {
|
func (s *StepMobile) SwipeToTapApp(appName string, opts ...option.ActionOption) *StepMobile {
|
||||||
action := option.MobileAction{
|
action := option.MobileAction{
|
||||||
Method: option.ACTION_SwipeToTapApp,
|
Method: option.ACTION_SwipeToTapApp,
|
||||||
|
|||||||
+262
-1
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/code"
|
"github.com/httprunner/httprunner/v5/code"
|
||||||
|
"github.com/httprunner/httprunner/v5/internal/simulation"
|
||||||
"github.com/httprunner/httprunner/v5/internal/utf7"
|
"github.com/httprunner/httprunner/v5/internal/utf7"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
@@ -532,7 +533,6 @@ func (ud *UIA2Driver) TouchByEvents(events []types.TouchEvent, opts ...option.Ac
|
|||||||
log.Warn().Int("action", event.Action).Msg("Unknown action type, skipping")
|
log.Warn().Int("action", event.Action).Msg("Unknown action type, skipping")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
actions = append(actions, actionMap)
|
actions = append(actions, actionMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,6 +553,201 @@ func (ud *UIA2Driver) TouchByEvents(events []types.TouchEvent, opts ...option.Ac
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SwipeWithDirection 向指定方向滑动任意距离
|
||||||
|
// direction: 滑动方向 ("up", "down", "left", "right")
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
// 获取设备型号和配置参数
|
||||||
|
deviceModel, _ := ud.Device.Model()
|
||||||
|
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||||
|
|
||||||
|
log.Info().Str("direction", direction).
|
||||||
|
Float64("startX", absStartX).Float64("startY", absStartY).
|
||||||
|
Float64("minDistance", simMinDistance).Float64("maxDistance", simMaxDistance).
|
||||||
|
Str("deviceModel", deviceModel).
|
||||||
|
Int("deviceID", deviceParams.DeviceID).
|
||||||
|
Float64("pressure", deviceParams.Pressure).
|
||||||
|
Float64("size", deviceParams.Size).
|
||||||
|
Msg("UIA2Driver.SwipeWithDirection")
|
||||||
|
|
||||||
|
// 导入滑动仿真库
|
||||||
|
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||||
|
|
||||||
|
// 转换方向字符串为Direction类型
|
||||||
|
var slideDirection simulation.Direction
|
||||||
|
switch direction {
|
||||||
|
case "up":
|
||||||
|
slideDirection = simulation.Up
|
||||||
|
case "down":
|
||||||
|
slideDirection = simulation.Down
|
||||||
|
case "left":
|
||||||
|
slideDirection = simulation.Left
|
||||||
|
case "right":
|
||||||
|
slideDirection = simulation.Right
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid direction: %s, must be one of: up, down, left, right", direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用滑动仿真算法生成触摸事件序列
|
||||||
|
events, err := simulator.GenerateSlideWithRandomDistance(
|
||||||
|
absStartX, absStartY, slideDirection, simMinDistance, simMaxDistance,
|
||||||
|
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate slide events failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行触摸事件序列
|
||||||
|
return ud.TouchByEvents(events, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwipeInArea 在指定区域内向指定方向滑动任意距离
|
||||||
|
// direction: 滑动方向 ("up", "down", "left", "right")
|
||||||
|
// 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, simAreaStartX, simAreaStartY)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
absAreaEndX, absAreaEndY, err := convertToAbsolutePoint(ud, simAreaEndX, simAreaEndY)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保区域坐标正确(start应该小于等于end)
|
||||||
|
if absAreaStartX > absAreaEndX {
|
||||||
|
absAreaStartX, absAreaEndX = absAreaEndX, absAreaStartX
|
||||||
|
}
|
||||||
|
if absAreaStartY > absAreaEndY {
|
||||||
|
absAreaStartY, absAreaEndY = absAreaEndY, absAreaStartY
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备型号和配置参数
|
||||||
|
deviceModel, _ := ud.Device.Model()
|
||||||
|
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||||
|
|
||||||
|
log.Info().Str("direction", direction).
|
||||||
|
Float64("areaStartX", absAreaStartX).Float64("areaStartY", absAreaStartY).
|
||||||
|
Float64("areaEndX", absAreaEndX).Float64("areaEndY", absAreaEndY).
|
||||||
|
Float64("minDistance", simMinDistance).Float64("maxDistance", simMaxDistance).
|
||||||
|
Str("deviceModel", deviceModel).
|
||||||
|
Int("deviceID", deviceParams.DeviceID).
|
||||||
|
Float64("pressure", deviceParams.Pressure).
|
||||||
|
Float64("size", deviceParams.Size).
|
||||||
|
Msg("UIA2Driver.SwipeInArea")
|
||||||
|
|
||||||
|
// 导入滑动仿真库
|
||||||
|
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||||
|
|
||||||
|
// 转换方向字符串为Direction类型
|
||||||
|
var slideDirection simulation.Direction
|
||||||
|
switch direction {
|
||||||
|
case "up":
|
||||||
|
slideDirection = simulation.Up
|
||||||
|
case "down":
|
||||||
|
slideDirection = simulation.Down
|
||||||
|
case "left":
|
||||||
|
slideDirection = simulation.Left
|
||||||
|
case "right":
|
||||||
|
slideDirection = simulation.Right
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid direction: %s, must be one of: up, down, left, right", direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用滑动仿真算法生成区域内滑动的触摸事件序列
|
||||||
|
events, err := simulator.GenerateSlideInArea(
|
||||||
|
absAreaStartX, absAreaStartY, absAreaEndX, absAreaEndY,
|
||||||
|
slideDirection, simMinDistance, simMaxDistance,
|
||||||
|
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate slide in area events failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行触摸事件序列
|
||||||
|
return ud.TouchByEvents(events, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwipeFromPointToPoint 指定起始点和结束点进行滑动
|
||||||
|
// fromX, fromY: 起始坐标(相对坐标)
|
||||||
|
// toX, toY: 结束坐标(相对坐标)
|
||||||
|
func (ud *UIA2Driver) SIMSwipeFromPointToPoint(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error {
|
||||||
|
// 转换起始点和结束点为绝对坐标
|
||||||
|
absStartX, absStartY, err := convertToAbsolutePoint(ud, fromX, fromY)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
absEndX, absEndY, err := convertToAbsolutePoint(ud, toX, toY)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备型号和配置参数
|
||||||
|
deviceModel, _ := ud.Device.Model()
|
||||||
|
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||||
|
|
||||||
|
log.Info().Float64("startX", absStartX).Float64("startY", absStartY).
|
||||||
|
Float64("endX", absEndX).Float64("endY", absEndY).
|
||||||
|
Str("deviceModel", deviceModel).
|
||||||
|
Int("deviceID", deviceParams.DeviceID).
|
||||||
|
Float64("pressure", deviceParams.Pressure).
|
||||||
|
Float64("size", deviceParams.Size).
|
||||||
|
Msg("UIA2Driver.SwipeFromPointToPoint")
|
||||||
|
|
||||||
|
// 导入滑动仿真库
|
||||||
|
simulator := simulation.NewSlideSimulatorAPI(nil)
|
||||||
|
|
||||||
|
// 使用滑动仿真算法生成点对点滑动的触摸事件序列
|
||||||
|
events, err := simulator.GeneratePointToPointSlideEvents(
|
||||||
|
absStartX, absStartY, absEndX, absEndY,
|
||||||
|
deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate point to point slide events failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行触摸事件序列
|
||||||
|
return ud.TouchByEvents(events, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClickAtPoint 点击相对坐标
|
||||||
|
// x, y: 点击坐标(相对坐标)
|
||||||
|
func (ud *UIA2Driver) SIMClickAtPoint(x, y float64, opts ...option.ActionOption) error {
|
||||||
|
// 转换为绝对坐标
|
||||||
|
absX, absY, err := convertToAbsolutePoint(ud, x, y)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备型号和配置参数
|
||||||
|
deviceModel, _ := ud.Device.Model()
|
||||||
|
deviceParams := simulation.GetRandomDeviceParams(deviceModel)
|
||||||
|
|
||||||
|
log.Info().Float64("x", absX).Float64("y", absY).
|
||||||
|
Str("deviceModel", deviceModel).
|
||||||
|
Int("deviceID", deviceParams.DeviceID).
|
||||||
|
Float64("pressure", deviceParams.Pressure).
|
||||||
|
Float64("size", deviceParams.Size).
|
||||||
|
Msg("UIA2Driver.ClickAtPoint")
|
||||||
|
|
||||||
|
// 导入点击仿真库
|
||||||
|
clickSimulator := simulation.NewClickSimulatorAPI(nil)
|
||||||
|
|
||||||
|
// 使用点击仿真算法生成触摸事件序列
|
||||||
|
events, err := clickSimulator.GenerateClickEvents(
|
||||||
|
absX, absY, deviceParams.DeviceID, deviceParams.Pressure, deviceParams.Size)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate click events failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行触摸事件序列
|
||||||
|
return ud.TouchByEvents(events, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
func (ud *UIA2Driver) SetPasteboard(contentType types.PasteboardType, content string) (err error) {
|
func (ud *UIA2Driver) SetPasteboard(contentType types.PasteboardType, content string) (err error) {
|
||||||
log.Info().Str("contentType", string(contentType)).
|
log.Info().Str("contentType", string(contentType)).
|
||||||
Str("content", content).Msg("UIA2Driver.SetPasteboard")
|
Str("content", content).Msg("UIA2Driver.SetPasteboard")
|
||||||
@@ -593,6 +788,72 @@ func (ud *UIA2Driver) Input(text string, opts ...option.ActionOption) (err error
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SIMInput 仿真输入函数,模拟人类分批输入行为
|
||||||
|
// 将文本智能分割,英文单词和数字保持完整,中文按1-2个字符分割
|
||||||
|
func (ud *UIA2Driver) SIMInput(text string, opts ...option.ActionOption) error {
|
||||||
|
log.Info().Str("text", text).Msg("UIA2Driver.SIMInput")
|
||||||
|
|
||||||
|
if text == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建输入仿真器(使用默认配置)
|
||||||
|
inputSimulator := simulation.NewInputSimulatorAPI(nil)
|
||||||
|
|
||||||
|
// 生成输入片段(使用智能分割算法,所有参数使用默认值)
|
||||||
|
inputReq := simulation.InputRequest{
|
||||||
|
Text: text,
|
||||||
|
// MinSegmentLen, MaxSegmentLen, MinDelayMs, MaxDelayMs 使用默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
response := inputSimulator.GenerateInputSegments(inputReq)
|
||||||
|
if !response.Success {
|
||||||
|
return fmt.Errorf("failed to generate input segments: %s", response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Int("segments", response.Metrics.TotalSegments).
|
||||||
|
Int("totalDelayMs", response.Metrics.TotalDelayMs).
|
||||||
|
Int("estimatedTimeMs", response.Metrics.EstimatedTimeMs).
|
||||||
|
Msg("Input segments generated")
|
||||||
|
|
||||||
|
// 逐个输入每个片段
|
||||||
|
var segmentErrCnt int
|
||||||
|
for _, segment := range response.Segments {
|
||||||
|
// 使用SendUnicodeKeys进行输入(内部已包含Session.POST请求)
|
||||||
|
segmentErr := ud.SendUnicodeKeys(segment.Text, opts...)
|
||||||
|
if segmentErr != nil {
|
||||||
|
segmentErrCnt++
|
||||||
|
log.Info().Err(segmentErr).Int("segmentErrCnt", segmentErrCnt).
|
||||||
|
Msg("segments err")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("segment", segment.Text).Int("index", segment.Index).
|
||||||
|
Int("charLen", segment.CharLen).Msg("Successfully input segment")
|
||||||
|
|
||||||
|
// 如果有延迟时间,则等待
|
||||||
|
if segment.DelayMs > 0 {
|
||||||
|
time.Sleep(time.Duration(segment.DelayMs) * time.Millisecond)
|
||||||
|
|
||||||
|
log.Debug().Int("delayMs", segment.DelayMs).
|
||||||
|
Msg("Delay between input segments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if segmentErrCnt > 0 {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"text": text,
|
||||||
|
}
|
||||||
|
option.MergeOptions(data, opts...)
|
||||||
|
urlStr := fmt.Sprintf("/session/%s/keys", ud.Session.ID)
|
||||||
|
_, err := ud.Session.POST(data, urlStr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info().Int("totalSegments", response.Metrics.TotalSegments).
|
||||||
|
Int("actualDelayMs", response.Metrics.TotalDelayMs).
|
||||||
|
Msg("SIMInput completed successfully")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ud *UIA2Driver) SendUnicodeKeys(text string, opts ...option.ActionOption) (err error) {
|
func (ud *UIA2Driver) SendUnicodeKeys(text string, opts ...option.ActionOption) (err error) {
|
||||||
log.Info().Str("text", text).Msg("UIA2Driver.SendUnicodeKeys")
|
log.Info().Str("text", text).Msg("UIA2Driver.SendUnicodeKeys")
|
||||||
// If the Unicode IME is not installed, fall back to the old interface.
|
// If the Unicode IME is not installed, fall back to the old interface.
|
||||||
|
|||||||
@@ -323,7 +323,6 @@ func createXTDriverWithConfig(config DriverCacheConfig) (*XTDriver, error) {
|
|||||||
// Default AI options
|
// Default AI options
|
||||||
aiOpts = []option.AIServiceOption{
|
aiOpts = []option.AIServiceOption{
|
||||||
option.WithCVService(option.CVServiceTypeVEDEM),
|
option.WithCVService(option.CVServiceTypeVEDEM),
|
||||||
option.WithLLMConfig(option.RecommendedConfigurations()["ui_focused"]),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ var (
|
|||||||
_ IDriver = (*WDADriver)(nil)
|
_ IDriver = (*WDADriver)(nil)
|
||||||
_ IDriver = (*HDCDriver)(nil)
|
_ IDriver = (*HDCDriver)(nil)
|
||||||
_ IDriver = (*BrowserDriver)(nil)
|
_ IDriver = (*BrowserDriver)(nil)
|
||||||
|
|
||||||
|
// Ensure drivers implement SIMSupport interface
|
||||||
|
_ SIMSupport = (*UIA2Driver)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
// current implemeted driver: ADBDriver, UIA2Driver, WDADriver, HDCDriver
|
// current implemeted driver: ADBDriver, UIA2Driver, WDADriver, HDCDriver
|
||||||
@@ -90,3 +93,13 @@ type IDriver interface {
|
|||||||
// clipboard operations
|
// clipboard operations
|
||||||
GetPasteboard() (string, error)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
"image/gif"
|
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
"math"
|
"math"
|
||||||
@@ -244,8 +243,8 @@ func getScreenShotBuffer(driver IDriver) (compressedBufSource *bytes.Buffer, err
|
|||||||
"take screenshot failed %v", err)
|
"take screenshot failed %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// compress screenshot
|
// compress screenshot with quality 95
|
||||||
compressBufSource, err := compressImageBufferWithOptions(bufSource, false, 800)
|
compressBufSource, err := compressImageBufferWithOptions(bufSource, 95)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(code.DeviceScreenShotError,
|
return nil, errors.Wrapf(code.DeviceScreenShotError,
|
||||||
"compress screenshot failed %v", err)
|
"compress screenshot failed %v", err)
|
||||||
@@ -262,11 +261,7 @@ func saveScreenShot(raw *bytes.Buffer, screenshotPath string) error {
|
|||||||
log.Error().Err(err).Msg("copy screenshot buffer failed")
|
log.Error().Err(err).Msg("copy screenshot buffer failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
img, format, err := image.Decode(copiedBuffer)
|
// create file
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "decode screenshot image failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Create(screenshotPath)
|
file, err := os.Create(screenshotPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "create screenshot image file failed")
|
return errors.Wrap(err, "create screenshot image file failed")
|
||||||
@@ -275,26 +270,10 @@ func saveScreenShot(raw *bytes.Buffer, screenshotPath string) error {
|
|||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// compress image and save to file
|
// directly write compressed JPEG data to avoid quality loss
|
||||||
switch format {
|
_, err = file.Write(copiedBuffer.Bytes())
|
||||||
case "jpeg":
|
|
||||||
jpegOptions := &jpeg.Options{Quality: 95}
|
|
||||||
err = jpeg.Encode(file, img, jpegOptions)
|
|
||||||
case "png":
|
|
||||||
encoder := png.Encoder{
|
|
||||||
CompressionLevel: png.BestCompression,
|
|
||||||
}
|
|
||||||
err = encoder.Encode(file, img)
|
|
||||||
case "gif":
|
|
||||||
gifOptions := &gif.Options{
|
|
||||||
NumColors: 256,
|
|
||||||
}
|
|
||||||
err = gif.Encode(file, img, gifOptions)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported image format %s", format)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "save image file failed")
|
return errors.Wrap(err, "write image file failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileSize int64
|
var fileSize int64
|
||||||
@@ -303,14 +282,14 @@ func saveScreenShot(raw *bytes.Buffer, screenshotPath string) error {
|
|||||||
fileSize = fileInfo.Size()
|
fileSize = fileInfo.Size()
|
||||||
}
|
}
|
||||||
log.Info().Str("path", screenshotPath).
|
log.Info().Str("path", screenshotPath).
|
||||||
Int("rawBytes", raw.Len()).Int64("saveBytes", fileSize).
|
Int64("fileSize", fileSize).
|
||||||
Msg("save screenshot file success")
|
Msg("save screenshot file success")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// compressImageBufferWithOptions compresses image buffer with advanced options
|
// compressImageBufferWithOptions compresses image buffer with advanced options
|
||||||
func compressImageBufferWithOptions(raw *bytes.Buffer, enableResize bool, maxWidth int) (compressed *bytes.Buffer, err error) {
|
func compressImageBufferWithOptions(raw *bytes.Buffer, quality int) (compressed *bytes.Buffer, err error) {
|
||||||
rawSize := raw.Len()
|
rawSize := raw.Len()
|
||||||
// decode image from buffer
|
// decode image from buffer
|
||||||
img, format, err := image.Decode(raw)
|
img, format, err := image.Decode(raw)
|
||||||
@@ -318,32 +297,12 @@ func compressImageBufferWithOptions(raw *bytes.Buffer, enableResize bool, maxWid
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get original image dimensions
|
|
||||||
bounds := img.Bounds()
|
|
||||||
originalWidth := bounds.Dx()
|
|
||||||
originalHeight := bounds.Dy()
|
|
||||||
|
|
||||||
// Calculate new dimensions for compression if resize is enabled
|
|
||||||
var newWidth, newHeight int
|
|
||||||
var resizedImg image.Image = img
|
|
||||||
|
|
||||||
if enableResize && originalWidth > maxWidth {
|
|
||||||
ratio := float64(maxWidth) / float64(originalWidth)
|
|
||||||
newWidth = maxWidth
|
|
||||||
newHeight = int(float64(originalHeight) * ratio)
|
|
||||||
resizedImg = resizeImage(img, newWidth, newHeight)
|
|
||||||
} else {
|
|
||||||
newWidth = originalWidth
|
|
||||||
newHeight = originalHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
jpegQuality := 95
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
switch format {
|
switch format {
|
||||||
case "jpeg", "jpg", "png":
|
case "jpeg", "jpg", "png":
|
||||||
// compress with compression rate of 95
|
// compress with compression rate
|
||||||
jpegOptions := &jpeg.Options{Quality: jpegQuality}
|
jpegOptions := &jpeg.Options{Quality: quality}
|
||||||
err = jpeg.Encode(&buf, resizedImg, jpegOptions)
|
err = jpeg.Encode(&buf, img, jpegOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -354,55 +313,18 @@ func compressImageBufferWithOptions(raw *bytes.Buffer, enableResize bool, maxWid
|
|||||||
compressedSize := buf.Len()
|
compressedSize := buf.Len()
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Int("rawSize", rawSize).
|
Int("rawSize", rawSize).
|
||||||
Int("originalWidth", originalWidth).
|
Int("quality", quality).
|
||||||
Int("originalHeight", originalHeight).
|
|
||||||
Int("newWidth", newWidth).
|
|
||||||
Int("newHeight", newHeight).
|
|
||||||
Int("jpegQuality", jpegQuality).
|
|
||||||
Int("compressedSize", compressedSize).
|
Int("compressedSize", compressedSize).
|
||||||
Bool("resized", enableResize && originalWidth > maxWidth).
|
|
||||||
Msg("compress image buffer")
|
Msg("compress image buffer")
|
||||||
|
|
||||||
// return compressed image buffer
|
// return compressed image buffer
|
||||||
return &buf, nil
|
return &buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resizeImage resizes an image using simple nearest neighbor algorithm
|
|
||||||
func resizeImage(src image.Image, width, height int) image.Image {
|
|
||||||
srcBounds := src.Bounds()
|
|
||||||
srcWidth := srcBounds.Dx()
|
|
||||||
srcHeight := srcBounds.Dy()
|
|
||||||
|
|
||||||
// Create a new image with the target dimensions
|
|
||||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
||||||
|
|
||||||
// Simple nearest neighbor resizing
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
// Map destination coordinates to source coordinates
|
|
||||||
srcX := x * srcWidth / width
|
|
||||||
srcY := y * srcHeight / height
|
|
||||||
|
|
||||||
// Ensure we don't go out of bounds
|
|
||||||
if srcX >= srcWidth {
|
|
||||||
srcX = srcWidth - 1
|
|
||||||
}
|
|
||||||
if srcY >= srcHeight {
|
|
||||||
srcY = srcHeight - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy pixel from source to destination
|
|
||||||
dst.Set(x, y, src.At(srcBounds.Min.X+srcX, srcBounds.Min.Y+srcY))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompressImageFile compresses an image file and returns the compressed data
|
// CompressImageFile compresses an image file and returns the compressed data
|
||||||
func CompressImageFile(imagePath string, enableResize bool, maxWidth int) ([]byte, error) {
|
func CompressImageFile(imagePath string, quality int) ([]byte, error) {
|
||||||
log.Debug().Str("imagePath", imagePath).Bool("enableResize", enableResize).
|
log.Debug().Str("imagePath", imagePath).
|
||||||
Int("maxWidth", maxWidth).Msg("compress image file")
|
Int("quality", quality).Msg("compress image file")
|
||||||
|
|
||||||
// Read the original image file
|
// Read the original image file
|
||||||
file, err := os.Open(imagePath)
|
file, err := os.Open(imagePath)
|
||||||
@@ -419,7 +341,7 @@ func CompressImageFile(imagePath string, enableResize bool, maxWidth int) ([]byt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compress using the buffer compression function
|
// Compress using the buffer compression function
|
||||||
compressedBuf, err := compressImageBufferWithOptions(&buf, enableResize, maxWidth)
|
compressedBuf, err := compressImageBufferWithOptions(&buf, quality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to compress image: %w", err)
|
return nil, fmt.Errorf("failed to compress image: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-9
@@ -87,23 +87,28 @@ func (s *MCPServer4XTDriver) registerTools() {
|
|||||||
s.registerTool(&ToolSelectDevice{}) // SelectDevice
|
s.registerTool(&ToolSelectDevice{}) // SelectDevice
|
||||||
|
|
||||||
// Touch Tools
|
// Touch Tools
|
||||||
s.registerTool(&ToolTapXY{}) // tap xy
|
s.registerTool(&ToolTapXY{}) // tap xy
|
||||||
s.registerTool(&ToolTapAbsXY{}) // tap abs xy
|
s.registerTool(&ToolTapAbsXY{}) // tap abs xy
|
||||||
s.registerTool(&ToolTapByOCR{}) // tap by OCR
|
s.registerTool(&ToolTapByOCR{}) // tap by OCR
|
||||||
s.registerTool(&ToolTapByCV{}) // tap by CV
|
s.registerTool(&ToolTapByCV{}) // tap by CV
|
||||||
s.registerTool(&ToolDoubleTapXY{}) // double tap xy
|
s.registerTool(&ToolDoubleTapXY{}) // double tap xy
|
||||||
|
s.registerTool(&ToolSIMClickAtPoint{}) // simulated click at point
|
||||||
|
|
||||||
// Swipe Tools
|
// Swipe Tools
|
||||||
s.registerTool(&ToolSwipe{}) // generic swipe, auto-detect direction or coordinate
|
s.registerTool(&ToolSwipe{}) // generic swipe, auto-detect direction or coordinate
|
||||||
s.registerTool(&ToolSwipeDirection{}) // swipe direction, up/down/left/right
|
s.registerTool(&ToolSwipeDirection{}) // swipe direction, up/down/left/right
|
||||||
s.registerTool(&ToolSwipeCoordinate{}) // swipe coordinate, [fromX, fromY, toX, toY]
|
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(&ToolSwipeToTapApp{})
|
||||||
s.registerTool(&ToolSwipeToTapText{})
|
s.registerTool(&ToolSwipeToTapText{})
|
||||||
s.registerTool(&ToolSwipeToTapTexts{})
|
s.registerTool(&ToolSwipeToTapTexts{})
|
||||||
s.registerTool(&ToolDrag{})
|
s.registerTool(&ToolDrag{})
|
||||||
|
|
||||||
// Input Tools
|
// Input Tools
|
||||||
s.registerTool(&ToolInput{})
|
s.registerTool(&ToolInput{}) // regular input
|
||||||
|
s.registerTool(&ToolSIMInput{}) // simulated input with intelligent segmentation
|
||||||
s.registerTool(&ToolBackspace{})
|
s.registerTool(&ToolBackspace{})
|
||||||
s.registerTool(&ToolSetIme{})
|
s.registerTool(&ToolSetIme{})
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -547,3 +547,412 @@ func (t *ToolDrag) ConvertActionToCallToolRequest(action option.MobileAction) (m
|
|||||||
}
|
}
|
||||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid drag parameters: %v", action.Params)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"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)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+27
-6
@@ -66,9 +66,14 @@ const (
|
|||||||
ACTION_TapByCV ActionName = "tap_cv"
|
ACTION_TapByCV ActionName = "tap_cv"
|
||||||
ACTION_DoubleTap ActionName = "double_tap" // generic double tap action
|
ACTION_DoubleTap ActionName = "double_tap" // generic double tap action
|
||||||
ACTION_DoubleTapXY ActionName = "double_tap_xy"
|
ACTION_DoubleTapXY ActionName = "double_tap_xy"
|
||||||
ACTION_Swipe ActionName = "swipe" // swipe by direction or coordinates
|
ACTION_Swipe ActionName = "swipe" // swipe by direction or coordinates
|
||||||
ACTION_SwipeDirection ActionName = "swipe_direction" // swipe by direction (up, down, left, right)
|
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_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_Drag ActionName = "drag"
|
||||||
ACTION_Input ActionName = "input"
|
ACTION_Input ActionName = "input"
|
||||||
ACTION_PressButton ActionName = "press_button"
|
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"`
|
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"`
|
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"`
|
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"`
|
// SIM specific options with SIM prefix
|
||||||
Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty" desc:"Action frequency"`
|
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
|
ScreenOptions
|
||||||
|
|
||||||
@@ -649,6 +663,13 @@ func (o *ActionOptions) GetMCPOptions(actionType ActionName) []mcp.ToolOption {
|
|||||||
ACTION_Back: {"platform", "serial"},
|
ACTION_Back: {"platform", "serial"},
|
||||||
ACTION_ListPackages: {"platform", "serial"},
|
ACTION_ListPackages: {"platform", "serial"},
|
||||||
ACTION_ClosePopups: {"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]
|
fields := fieldMappings[actionType]
|
||||||
|
|||||||
Reference in New Issue
Block a user