fix: merge

This commit is contained in:
huangbin.beal
2025-08-06 10:52:49 +08:00
19 changed files with 2122 additions and 155 deletions

View File

@@ -26,6 +26,7 @@ type WingsService struct {
bizId string
accessKey string
secretKey string
history []History // Conversation history for Wings API
}
// NewWingsService creates a new Wings service instance
@@ -49,6 +50,7 @@ func NewWingsService() (ILLMService, error) {
bizId: bizID,
accessKey: accessKey,
secretKey: secretKey,
history: []History{},
}, nil
}
@@ -59,6 +61,11 @@ func (w *WingsService) Plan(ctx context.Context, opts *PlanningOptions) (*Planni
return nil, errors.Wrap(err, "validate planning parameters failed")
}
// Reset history if requested
if opts.ResetHistory {
w.resetHistory()
}
// Extract screenshot from message
screenshot, err := w.extractScreenshotFromMessage(opts.Message)
if err != nil {
@@ -70,15 +77,11 @@ func (w *WingsService) Plan(ctx context.Context, opts *PlanningOptions) (*Planni
// Prepare Wings API request
apiRequest := WingsActionRequest{
Historys: []interface{}{}, // empty as specified
DeviceInfos: []WingsDeviceInfo{
deviceInfo,
},
StepText: opts.UserInstruction,
BizId: w.bizId,
TextCase: "整体描述:\\n前置条件\\n获取 1 台设备 A。\\n获取 1 个[万粉创作者]账号a。\\n获取 2 个[普通]账号 b、c。\\n账号 a 和账号 b 互相关注。\\n账号 a 和账号 c 互相关注。\\n账号 a 给账号 b 设置备注为 “11131b”。\\n账号 a 给账号 c 设置备注为 “11131c”。\\n账号 a 创建一个粉丝群 m。\\n 账号 a 修改粉丝群 m 名称为“11131群”。\\n 账号 a 邀请账号 b 加入粉丝群 m。\\n账号 a 邀请账号 c 加入粉丝群 m。\\n账号 a 给群聊 m 发送一条文字消息。\\n设备 A 打开抖音 app。\\n设备 A 登录账号 a。\\n设备 A 退出抖音 app。\\n操作步骤\\n账号a打开抖音app。\\n点击“消息”。\\n点击“11131群”cell。\\n点击“聊天信息页入口”按钮。\\n点击“分享公开群”按钮。\\n点击文字“群口令”。\\n断言屏幕中存在文字“口令复制成功”。\\n停止操作。\\n注意事项\\n",
StepType: "automation",
DeviceID: deviceInfo.DeviceID,
Historys: w.history,
DeviceInfo: deviceInfo,
StepText: fmt.Sprintf("%s", opts.UserInstruction),
BizId: w.bizId,
TextCase: fmt.Sprintf("整体描述:\n前置条件\n操作步骤\n%s\n停止操作。\n注意事项\n", opts.UserInstruction),
Base: WingsBase{
LogID: generateWingsUUID(),
},
@@ -98,7 +101,7 @@ func (w *WingsService) Plan(ctx context.Context, opts *PlanningOptions) (*Planni
}
// Check API response status
if response.BaseResp.StatusCode != 0 {
if response.BaseResp.StatusCode != 0 && response.BaseResp.StatusCode != 200 {
err = fmt.Errorf("API returned error: %s", response.BaseResp.StatusMessage)
return &PlanningResult{
Thought: response.ThoughtChain.Thought,
@@ -107,26 +110,50 @@ func (w *WingsService) Plan(ctx context.Context, opts *PlanningOptions) (*Planni
}, err
}
// Convert Wings API response to tool calls
toolCalls, err := w.convertWingsResponseToToolCalls(response.ActionParams)
if err != nil {
return &PlanningResult{
Thought: response.ThoughtChain.Thought,
Error: err.Error(),
ModelName: "wings-api",
}, errors.Wrap(err, "convert Wings response to tool calls failed")
// Update history with response data
newHistoryEntry := History{
Observation: response.ThoughtChain.Observation,
Thought: response.ThoughtChain.Thought,
Summary: response.ThoughtChain.Summary,
StepText: response.StepText,
StepTextTrans: response.StepTextTrans,
OriStepIndex: response.OriStepIndex,
DeviceID: deviceInfo[0].DeviceID,
AgentType: response.AgentType,
ActionResult: "", // Always empty as requested
DeviceInfos: &deviceInfo,
ActionParams: response.ActionParams,
}
w.history = append(w.history, newHistoryEntry)
var toolCalls []schema.ToolCall
if response.StepType != "FINISH" {
// Convert Wings API response to tool calls
toolCalls, err = w.convertWingsResponseToToolCalls(response.ActionParams)
if err != nil {
return &PlanningResult{
Thought: response.ThoughtChain.Thought,
Error: err.Error(),
ModelName: "wings-api",
}, errors.Wrap(err, "convert Wings response to tool calls failed")
}
}
// No need to update ActionResult as per user request
// ActionResult should always be empty
log.Info().
Str("thought", response.ThoughtChain.Thought).
Str("action", response.AgentType).
Str("action_params", response.ActionParams).
Str("log_id", fmt.Sprintf("%v", response.BaseResp.Extra)).
Int("tool_calls_count", len(toolCalls)).
Int64("elapsed_ms", elapsed).
Msg("Wings API planning completed")
return &PlanningResult{
ToolCalls: toolCalls,
Thought: response.ThoughtChain.Thought,
Content: response.ThoughtChain.Summary,
Thought: response.StepTextTrans,
Content: response.StepTextTrans,
ModelName: "wings-api",
}, nil
}
@@ -146,20 +173,15 @@ func (w *WingsService) Assert(ctx context.Context, opts *AssertOptions) (*Assert
// Prepare Wings API request for assertion
apiRequest := WingsActionRequest{
Historys: []interface{}{}, // empty as specified
DeviceInfos: []WingsDeviceInfo{
deviceInfo,
},
StepText: opts.Assertion,
BizId: w.bizId,
TextCase: "整体描述:\\n前置条件\\n获取 1 台设备 A。\\n获取 1 个[万粉创作者]账号a。\\n获取 2 个[普通]账号 b、c。\\n账号 a 和账号 b 互相关注。\\n账号 a 和账号 c 互相关注。\\n账号 a 给账号 b 设置备注为 “11131b”。\\n账号 a 给账号 c 设置备注为 “11131c”。\\n账号 a 创建一个粉丝群 m。\\n 账号 a 修改粉丝群 m 名称为“11131群”。\\n 账号 a 邀请账号 b 加入粉丝群 m。\\n账号 a 邀请账号 c 加入粉丝群 m。\\n账号 a 给群聊 m 发送一条文字消息。\\n设备 A 打开抖音 app。\\n设备 A 登录账号 a。\\n设备 A 退出抖音 app。\\n操作步骤\\n账号a打开抖音app。\\n点击“消息”。\\n点击“11131群”cell。\\n点击“聊天信息页入口”按钮。\\n点击“分享公开群”按钮。\\n点击文字“群口令”。\\n断言屏幕中存在文字“口令复制成功”。\\n停止操作。\\n注意事项\\n",
StepType: "assert", // Different from automation
DeviceID: deviceInfo.DeviceID,
Historys: []History{},
DeviceInfo: deviceInfo,
StepText: fmt.Sprintf("断言:%s", opts.Assertion),
BizId: w.bizId,
TextCase: fmt.Sprintf("整体描述:\n前置条件\n操作步骤\n断言: %s\n停止操作。\n注意事项\n", opts.Assertion),
Base: WingsBase{
LogID: generateWingsUUID(),
},
}
log.Info().Interface("apiRequest", apiRequest).Msg("Wings API request")
// Call Wings API
startTime := time.Now()
@@ -175,7 +197,7 @@ func (w *WingsService) Assert(ctx context.Context, opts *AssertOptions) (*Assert
}
// Check API response status
if response.BaseResp.StatusCode != 0 {
if response.BaseResp.StatusCode != 0 && response.BaseResp.StatusCode != 200 {
err = fmt.Errorf("API returned error: %s", response.BaseResp.StatusMessage)
return &AssertionResult{
Pass: false,
@@ -184,6 +206,22 @@ func (w *WingsService) Assert(ctx context.Context, opts *AssertOptions) (*Assert
}, err
}
// Update history with response data
newHistoryEntry := History{
Observation: response.ThoughtChain.Observation,
Thought: response.ThoughtChain.Thought,
Summary: response.ThoughtChain.Summary,
StepText: response.StepText,
StepTextTrans: response.StepTextTrans,
OriStepIndex: response.OriStepIndex,
DeviceID: deviceInfo[0].DeviceID,
AgentType: response.AgentType,
ActionResult: "", // Always empty as requested
DeviceInfos: &deviceInfo,
ActionParams: response.ActionParams,
}
w.history = append(w.history, newHistoryEntry)
// Parse assertion result from action_params
passed, assertionThought, err := w.parseAssertionResult(response.ActionParams, response.ThoughtChain)
if err != nil {
@@ -194,6 +232,9 @@ func (w *WingsService) Assert(ctx context.Context, opts *AssertOptions) (*Assert
}, errors.Wrap(err, "parse assertion result failed")
}
// No need to update ActionResult as per user request
// ActionResult should always be empty
log.Info().
Bool("passed", passed).
Str("thought", assertionThought).
@@ -228,14 +269,12 @@ func (w *WingsService) RegisterTools(tools []*schema.ToolInfo) error {
// Wings API data structures
type WingsActionRequest struct {
Historys []interface{} `json:"historys"`
DeviceInfos []WingsDeviceInfo `json:"device_infos"`
StepText string `json:"step_text"`
BizId string `json:"biz_id"`
TextCase string `json:"text_case"`
StepType string `json:"step_type"`
DeviceID string `json:"device_id"`
Base WingsBase `json:"Base"`
Historys []History `json:"historys"`
DeviceInfo []WingsDeviceInfo `json:"device_infos"`
StepText string `json:"step_text"`
BizId string `json:"biz_id"`
TextCase string `json:"text_case"`
Base WingsBase `json:"Base"`
}
type WingsDeviceInfo struct {
@@ -253,10 +292,14 @@ type WingsBase struct {
}
type WingsActionResponse struct {
StepType string `json:"step_type"`
ActionParams string `json:"action_params"`
ThoughtChain WingsThoughtChain `json:"thought_chain"`
BaseResp WingsBaseResp `json:"BaseResp"`
AgentType string `json:"agent_type" thrift:"agent_type,1,required"`
StepText string `json:"step_text" thrift:"step_text,2,required"`
StepTextTrans string `json:"step_text_trans" thrift:"step_text_trans,3,required"`
OriStepIndex int `json:"ori_step_index" thrift:"ori_step_index,4,required"`
StepType string `json:"step_type" thrift:"step_type,5,required"`
ActionParams string `json:"action_params" thrift:"action_params,6,required"`
ThoughtChain WingsThoughtChain `json:"thought_chain" thrift:"thought_chain,7,required"`
BaseResp WingsBaseResp `json:"BaseResp" thrift:"BaseResp,255,optional"`
}
type WingsThoughtChain struct {
@@ -276,6 +319,21 @@ type WingsExtra struct {
LogID string `json:"_log_id"`
}
// History structure for request and response
type History struct {
Observation string `json:"observation" thrift:"observation,1,required"` // 思考结果
Thought string `json:"thought" thrift:"thought,2,required"` // 思考结果
Summary string `json:"summary" thrift:"summary,3,required"` // 思考结果
StepText string `json:"step_text" thrift:"step_text,4"` // 操作的指令
DeviceID string `json:"device_id" thrift:"device_id,5"` // 操作的设备id
AgentType string `json:"agent_type" thrift:"agent_type,7"` // 最终决策的agent类型
ActionResult string `json:"action_result" thrift:"action_result,8"` // 操作结果, 断言=断言结果, 自动化=自动化操作是否成功, 物料构造=物料构造结果
DeviceInfos *[]WingsDeviceInfo `json:"device_infos,omitempty" thrift:"device_infos,9"` // 所有设备的信息
ActionParams string `json:"action_params,omitempty" thrift:"action_params,10"` // 历史操作解析结果(断言,自动化,物料构造)
StepTextTrans string `json:"step_text_trans,omitempty" thrift:"step_text_trans,13"` // 归一化的步骤文本(为后续的实际执行解析文本)
OriStepIndex int `json:"ori_step_index,omitempty" thrift:"ori_step_index,14"` // 原本的执行序列(扩展前、目标导向原始文本步骤)
}
// Action parameter structures
type WingsActionParams struct {
Type string `json:"Type"`
@@ -315,6 +373,11 @@ type WingsTextParams struct {
// Helper methods
// resetHistory resets the conversation history
func (w *WingsService) resetHistory() {
w.history = []History{}
}
// generateWingsUUID generates a random UUID for LogID
func generateWingsUUID() string {
return uuid.New().String()
@@ -345,19 +408,29 @@ func (w *WingsService) extractScreenshotFromMessage(message *schema.Message) (st
}
// getDeviceInfoFromContext gets device info from context with fallback
func (w *WingsService) getDeviceInfoFromContext(_ context.Context, screenshot string) WingsDeviceInfo {
// use default device info
return WingsDeviceInfo{
DeviceID: "default-device",
NowImage: screenshot,
PreImage: screenshot,
NowLayoutJSON: "",
OperationSystem: "android",
func (w *WingsService) getDeviceInfoFromContext(_ context.Context, screenshot string) []WingsDeviceInfo {
// TODO: Extract device info from context if available
// Use last history's NowImage as PreImage if history exists
preImage := screenshot
if len(w.history) > 0 && w.history[len(w.history)-1].DeviceInfos != nil && len(*w.history[len(w.history)-1].DeviceInfos) > 0 {
preImage = (*w.history[len(w.history)-1].DeviceInfos)[0].NowImage
}
// use default device info with optimized PreImage
return []WingsDeviceInfo{
{
DeviceID: "default-device",
NowImage: screenshot,
PreImage: preImage,
NowLayoutJSON: "",
OperationSystem: "android",
},
}
}
// getDeviceInfoFromScreenshot gets device info from screenshot (for Assert)
func (w *WingsService) getDeviceInfoFromScreenshot(ctx context.Context, screenshot string) WingsDeviceInfo {
func (w *WingsService) getDeviceInfoFromScreenshot(ctx context.Context, screenshot string) []WingsDeviceInfo {
return w.getDeviceInfoFromContext(ctx, screenshot)
}
@@ -390,6 +463,8 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Add("x-use-ppe", "1")
httpReq.Header.Add("x-tt-env", "ppe_refactor_merge")
// Add authentication headers if using external API
if w.accessKey != "" && w.secretKey != "" {
@@ -403,7 +478,7 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
// Execute HTTP request
client := &http.Client{
Timeout: 60 * time.Second,
Timeout: 120 * time.Second,
}
resp, err := client.Do(httpReq)
@@ -411,7 +486,9 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
return nil, errors.Wrap(err, "HTTP request failed")
}
defer resp.Body.Close()
// resp X-Tt-Logid
logID := resp.Header.Get("X-Tt-Logid")
log.Info().Str("step_text", request.StepText).Str("log_id", logID).Str("biz_id", request.BizId).Str("url", w.apiURL).Msg("call wings api")
// Read response body
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
@@ -434,7 +511,7 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
// convertWingsResponseToToolCalls converts Wings API response to tool calls using generic approach
func (w *WingsService) convertWingsResponseToToolCalls(actionParamsStr string) ([]schema.ToolCall, error) {
if actionParamsStr == "" {
if actionParamsStr == "" || actionParamsStr == "FINISH" {
return []schema.ToolCall{}, nil
}

View File

@@ -240,12 +240,12 @@ func (dev *AndroidDevice) installViaInstaller(apkPath string, args ...string) er
return err
}
// 等待安装完成或超时
timeout := 3 * time.Minute
timeout := 8 * time.Minute
select {
case err := <-done:
return err
case <-time.After(timeout):
return fmt.Errorf("installation timed out after %v", timeout)
return fmt.Errorf("install via installer timed out after %v", timeout)
}
}

View File

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

View File

@@ -21,11 +21,6 @@ func setupADBDriverExt(t *testing.T) *XTDriver {
Serial: "", // Let it auto-detect the device serial
AIOptions: []option.AIServiceOption{
option.WithCVService(option.CVServiceTypeVEDEM),
option.WithLLMConfig(
option.NewLLMServiceConfig(option.DOUBAO_1_5_UI_TARS_250328).
WithPlannerModel(option.WINGS_SERVICE).
WithAsserterModel(option.WINGS_SERVICE),
),
},
}

View File

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

View File

@@ -292,31 +292,14 @@ func TestDriverExt_AIAction(t *testing.T) {
func TestDriverExt_AIAction_CompareWithAIAction(t *testing.T) {
driver := setupDriverExt(t)
prompt := "点击搜索按钮"
prompt := "[目标导向]向上滑动屏幕2次"
// Test both methods with the same prompt
aiResult, aiErr := driver.AIAction(context.Background(), prompt)
aiResult, aiErr := driver.StartToGoal(context.Background(), prompt)
// Both should execute without critical errors (may have different implementations)
t.Logf("AIAction error: %v", aiErr)
// If both succeed, compare results
if aiResult != nil {
assert.Equal(t, "action", aiResult.Type, "AIAction result type should be 'action'")
// Both should have timing information
assert.Greater(t, aiResult.ModelCallElapsed, int64(0), "AIAction should have model call elapsed time")
// Both should have screenshot information
assert.NotEmpty(t, aiResult.ImagePath, "AIAction should have image path")
// Compare model names
if aiResult.PlanningResult != nil {
t.Logf("AIAction model: %s", aiResult.PlanningResult.ModelName)
assert.Equal(t, "wings-api", aiResult.PlanningResult.ModelName, "AIAction should use wings-api")
}
}
t.Logf("AIAction result: %v", aiResult)
}
// TestDriverExt_AIAction_ErrorHandling tests AIAction error handling

View File

@@ -620,6 +620,129 @@ func (wd *WDADriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Action
return wd.Drag(fromX, fromY, toX, toY, opts...)
}
// TouchByEvents performs a complex swipe using a sequence of touch events with pressure and size data
func (wd *WDADriver) TouchByEvents(events []types.TouchEvent, opts ...option.ActionOption) error {
log.Info().Int("eventCount", len(events)).Msg("WDADriver.SwipeSimulator")
if len(events) == 0 {
return fmt.Errorf("no touch events provided")
}
actionOptions := option.NewActionOptions(opts...)
// Apply pre-handlers for the first and last events (start and end coordinates)
firstEvent := events[0]
lastEvent := events[len(events)-1]
// Use rawX/rawY if available, otherwise fallback to X/Y for first event
startX, startY := firstEvent.RawX, firstEvent.RawY
if startX == 0 && startY == 0 {
startX, startY = firstEvent.X, firstEvent.Y
}
// Use rawX/rawY if available, otherwise fallback to X/Y for last event
endX, endY := lastEvent.RawX, lastEvent.RawY
if endX == 0 && endY == 0 {
endX, endY = lastEvent.X, lastEvent.Y
}
fromX, fromY, toX, toY, err := preHandler_Swipe(wd, option.ACTION_SwipeCoordinate, actionOptions,
startX, startY, endX, endY)
if err != nil {
return err
}
defer postHandler(wd, option.ACTION_SwipeCoordinate, actionOptions)
var actions []interface{}
var prevEventTime int64
for i, event := range events {
var duration float64
if i > 0 {
// Calculate duration from previous event using EventTime (milliseconds)
duration = float64(event.EventTime - prevEventTime)
}
prevEventTime = event.EventTime
// Use rawX/rawY if available, otherwise fallback to X/Y
x, y := event.RawX, event.RawY
if x == 0 && y == 0 {
// Fallback to X/Y if rawX/rawY are not set
x, y = event.X, event.Y
}
// Apply coordinate transformation if it's the first or last event
if i == 0 {
x, y = fromX, fromY
} else if i == len(events)-1 {
x, y = toX, toY
}
var actionMap map[string]interface{}
switch event.Action {
case 0: // ACTION_DOWN
actionMap = map[string]interface{}{
"type": "pointerDown",
"duration": 0,
"button": 0,
"pressure": event.Pressure,
"size": event.Size,
}
// Add initial move to position before down
if i == 0 {
moveAction := map[string]interface{}{
"type": "pointerMove",
"duration": 0,
"x": x,
"y": y,
"origin": "viewport",
"pressure": event.Pressure,
"size": event.Size,
}
actions = append(actions, moveAction)
}
case 1: // ACTION_UP
actionMap = map[string]interface{}{
"type": "pointerUp",
"duration": 0,
"button": 0,
"pressure": event.Pressure,
"size": event.Size,
}
case 2: // ACTION_MOVE
actionMap = map[string]interface{}{
"type": "pointerMove",
"duration": duration,
"x": x,
"y": y,
"origin": "viewport",
"pressure": event.Pressure,
"size": event.Size,
}
default:
log.Warn().Int("action", event.Action).Msg("Unknown action type, skipping")
continue
}
actions = append(actions, actionMap)
}
data := map[string]interface{}{
"actions": []interface{}{
map[string]interface{}{
"type": "pointer",
"parameters": map[string]string{"pointerType": "touch"},
"id": "touch",
"actions": actions,
},
},
}
option.MergeOptions(data, opts...)
_, err = wd.Session.POST(data, "/wings/actions")
return err
}
func (wd *WDADriver) SetPasteboard(contentType types.PasteboardType, content string) (err error) {
// [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)]
data := map[string]interface{}{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,618 @@
//go:build localtest
package uixt
import (
"fmt"
"strconv"
"strings"
"testing"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/types"
)
// ParseTouchEvents parses touch event data from comma-separated string format
func ParseTouchEvents(data string) ([]types.TouchEvent, error) {
lines := strings.Split(strings.TrimSpace(data), "\n")
events := make([]types.TouchEvent, 0, len(lines))
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
parts := strings.Split(line, ",")
if len(parts) != 13 {
return nil, fmt.Errorf("invalid touch event data format: expected 13 fields, got %d", len(parts))
}
event := types.TouchEvent{}
var err error
if event.X, err = strconv.ParseFloat(parts[1], 64); err != nil {
return nil, fmt.Errorf("invalid x coordinate: %v", err)
}
if event.Y, err = strconv.ParseFloat(parts[2], 64); err != nil {
return nil, fmt.Errorf("invalid y coordinate: %v", err)
}
if event.DeviceID, err = strconv.Atoi(parts[3]); err != nil {
return nil, fmt.Errorf("invalid device id: %v", err)
}
if event.Pressure, err = strconv.ParseFloat(parts[4], 64); err != nil {
return nil, fmt.Errorf("invalid pressure: %v", err)
}
if event.Size, err = strconv.ParseFloat(parts[5], 64); err != nil {
return nil, fmt.Errorf("invalid size: %v", err)
}
if event.RawX, err = strconv.ParseFloat(parts[6], 64); err != nil {
return nil, fmt.Errorf("invalid raw x: %v", err)
}
if event.RawY, err = strconv.ParseFloat(parts[7], 64); err != nil {
return nil, fmt.Errorf("invalid raw y: %v", err)
}
if event.DownTime, err = strconv.ParseInt(parts[8], 10, 64); err != nil {
return nil, fmt.Errorf("invalid down time: %v", err)
}
if event.EventTime, err = strconv.ParseInt(parts[9], 10, 64); err != nil {
return nil, fmt.Errorf("invalid event time: %v", err)
}
if event.ToolType, err = strconv.Atoi(parts[10]); err != nil {
return nil, fmt.Errorf("invalid tool type: %v", err)
}
if event.Flag, err = strconv.Atoi(parts[11]); err != nil {
return nil, fmt.Errorf("invalid flag: %v", err)
}
if event.Action, err = strconv.Atoi(parts[12]); err != nil {
return nil, fmt.Errorf("invalid action: %v", err)
}
events = append(events, event)
}
return events, nil
}
func TestAndroidTouchByEvents(t *testing.T) {
device, err := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := NewUIA2Driver(device)
if err != nil {
t.Fatal(err)
}
defer driver.TearDown()
// Example touch event data as provided
touchEventData := `1752649131556,401.20703,1191.3164,2,1.0,0.03529412,457.20703,1359.3164,111586196,111586196,1,0,0
1752649131595,402.913,1185.0792,2,1.0,0.039215688,458.913,1353.0792,111586196,111586236,1,0,2
1752649131612,410.60825,1164.3806,2,1.0,0.03529412,466.60825,1332.3806,111586196,111586250,1,0,2
1752649131629,437.7335,1093.1417,2,1.0,0.039215688,493.7335,1261.1417,111586196,111586270,1,0,2
1752649131646,463.5786,1018.01746,2,1.0,0.039215688,519.5786,1186.0175,111586196,111586287,1,0,2
1752649131662,487.56482,948.9773,2,1.0,0.03529412,543.5648,1116.9773,111586196,111586304,1,0,2
1752649131679,511.81476,881.6183,2,1.0,0.039215688,567.81476,1049.6183,111586196,111586320,1,0,2
1752649131696,543.4369,811.4982,2,1.0,0.03529412,599.4369,979.4982,111586196,111586337,1,0,2
1752649131713,577.1632,747.4512,2,1.0,0.039215688,633.1632,915.4512,111586196,111586354,1,0,2
1752649131729,610.1538,691.72034,2,1.0,0.03529412,666.1538,859.72034,111586196,111586370,1,0,2
1752649131746,639.1683,642.6914,2,1.0,0.03529412,695.1683,810.6914,111586196,111586387,1,0,2
1752649131763,658.9832,605.90857,2,1.0,0.03529412,714.9832,773.90857,111586196,111586404,1,0,2
1752649131779,672.21954,581.1634,2,1.0,0.03529412,728.21954,749.1634,111586196,111586420,1,0,2
1752649131796,680.7687,566.1778,2,1.0,0.03529412,736.7687,734.1778,111586196,111586434,1,0,2
1752649131814,688.0894,554.2295,2,1.0,0.03529412,744.0894,722.2295,111586196,111586450,1,0,2
1752649131830,694.542,544.7783,2,1.0,0.03529412,750.542,712.7783,111586196,111586466,1,0,2
1752649131847,700.60645,537.2637,2,1.0,0.039215688,756.60645,705.2637,111586196,111586483,1,0,2
1752649131863,705.08887,531.1406,2,1.0,0.039215688,761.08887,699.1406,111586196,111586500,1,0,2
1752649131880,708.1211,527.8008,2,1.0,0.039215688,764.1211,695.8008,111586196,111586517,1,0,2
1752649131897,709.43945,524.46094,2,1.0,0.039215688,765.43945,692.46094,111586196,111586533,1,0,2
1752649131902,709.1758,523.34766,2,1.0,0.03529412,765.1758,691.34766,111586196,111586537,1,33554432,2
1752649131907,709.1758,523.34766,2,1.0,0.03529412,765.1758,691.34766,111586196,111586546,1,0,1`
// Parse touch events
events, err := ParseTouchEvents(touchEventData)
if err != nil {
t.Fatalf("ParseTouchEvents failed: %v", err)
}
// Check first event
firstEvent := events[0]
if firstEvent.Action != 0 { // ACTION_DOWN
t.Errorf("Expected first event action to be 0 (ACTION_DOWN), got %d", firstEvent.Action)
}
// Check last event
lastEvent := events[len(events)-1]
if lastEvent.Action != 1 { // ACTION_UP
t.Errorf("Expected last event action to be 1 (ACTION_UP), got %d", lastEvent.Action)
}
// Use TouchByEvents with parsed events
err = driver.TouchByEvents(events)
if err != nil {
t.Fatalf("TouchByEvents failed: %v", err)
}
t.Logf("Successfully executed touch events: %d events processed", len(events))
}
func TestIOSTouchByEvents(t *testing.T) {
driver := setupWDADriverExt(t)
// Example touch event data as provided
touchEventData := `1752649131556,401.20703,1191.3164,2,1.0,0.03529412,457.20703,1359.3164,111586196,111586196,1,0,0
1752649131595,402.913,1185.0792,2,1.0,0.039215688,458.913,1353.0792,111586196,111586236,1,0,2
1752649131612,410.60825,1164.3806,2,1.0,0.03529412,466.60825,1332.3806,111586196,111586250,1,0,2
1752649131629,437.7335,1093.1417,2,1.0,0.039215688,493.7335,1261.1417,111586196,111586270,1,0,2
1752649131646,463.5786,1018.01746,2,1.0,0.039215688,519.5786,1186.0175,111586196,111586287,1,0,2
1752649131662,487.56482,948.9773,2,1.0,0.03529412,543.5648,1116.9773,111586196,111586304,1,0,2
1752649131679,511.81476,881.6183,2,1.0,0.039215688,567.81476,1049.6183,111586196,111586320,1,0,2
1752649131696,543.4369,811.4982,2,1.0,0.03529412,599.4369,979.4982,111586196,111586337,1,0,2
1752649131713,577.1632,747.4512,2,1.0,0.039215688,633.1632,915.4512,111586196,111586354,1,0,2
1752649131729,610.1538,691.72034,2,1.0,0.03529412,666.1538,859.72034,111586196,111586370,1,0,2
1752649131746,639.1683,642.6914,2,1.0,0.03529412,695.1683,810.6914,111586196,111586387,1,0,2
1752649131763,658.9832,605.90857,2,1.0,0.03529412,714.9832,773.90857,111586196,111586404,1,0,2
1752649131779,672.21954,581.1634,2,1.0,0.03529412,728.21954,749.1634,111586196,111586420,1,0,2
1752649131796,680.7687,566.1778,2,1.0,0.03529412,736.7687,734.1778,111586196,111586434,1,0,2
1752649131814,688.0894,554.2295,2,1.0,0.03529412,744.0894,722.2295,111586196,111586450,1,0,2
1752649131830,694.542,544.7783,2,1.0,0.03529412,750.542,712.7783,111586196,111586466,1,0,2
1752649131847,700.60645,537.2637,2,1.0,0.039215688,756.60645,705.2637,111586196,111586483,1,0,2
1752649131863,705.08887,531.1406,2,1.0,0.039215688,761.08887,699.1406,111586196,111586500,1,0,2
1752649131880,708.1211,527.8008,2,1.0,0.039215688,764.1211,695.8008,111586196,111586517,1,0,2
1752649131897,709.43945,524.46094,2,1.0,0.039215688,765.43945,692.46094,111586196,111586533,1,0,2
1752649131902,709.1758,523.34766,2,1.0,0.03529412,765.1758,691.34766,111586196,111586537,1,33554432,2
1752649131907,709.1758,523.34766,2,1.0,0.03529412,765.1758,691.34766,111586196,111586546,1,0,1`
// Parse touch events
events, err := ParseTouchEvents(touchEventData)
if err != nil {
t.Fatalf("ParseTouchEvents failed: %v", err)
}
// Check first event
firstEvent := events[0]
if firstEvent.Action != 0 { // ACTION_DOWN
t.Errorf("Expected first event action to be 0 (ACTION_DOWN), got %d", firstEvent.Action)
}
// Check last event
lastEvent := events[len(events)-1]
if lastEvent.Action != 1 { // ACTION_UP
t.Errorf("Expected last event action to be 1 (ACTION_UP), got %d", lastEvent.Action)
}
// Use TouchByEvents with parsed events
err = driver.IDriver.(*WDADriver).TouchByEvents(events)
if err != nil {
t.Fatalf("TouchByEvents failed: %v", err)
}
t.Logf("Successfully executed touch events: %d events processed", len(events))
}
func TestTouchEventParsing(t *testing.T) {
// Test single touch event parsing
singleEventData := "1752646457403,456.78418,1574.0195,7,1.0,0.016666668,504.78418,1721.0195,924451292,924451292,1,0,0"
events, err := ParseTouchEvents(singleEventData)
if err != nil {
t.Fatalf("ParseTouchEvents failed: %v", err)
}
if len(events) != 1 {
t.Fatalf("Expected 1 event, got %d", len(events))
}
event := events[0]
if event.X != 456.78418 {
t.Errorf("Expected X 456.78418, got %f", event.X)
}
if event.Y != 1574.0195 {
t.Errorf("Expected Y 1574.0195, got %f", event.Y)
}
if event.Action != 0 {
t.Errorf("Expected Action 0, got %d", event.Action)
}
if event.Pressure != 1.0 {
t.Errorf("Expected Pressure 1.0, got %f", event.Pressure)
}
if event.Size != 0.016666668 {
t.Errorf("Expected Size 0.016666668, got %f", event.Size)
}
}
func TestTouchEventParsingInvalidData(t *testing.T) {
// Test with invalid data
testCases := []struct {
name string
data string
}{
{
name: "too few fields",
data: "1752646457403,456.78418,1574.0195,7,1.0",
},
{
name: "invalid timestamp",
data: "invalid,456.78418,1574.0195,7,1.0,0.016666668,504.78418,1721.0195,924451292,924451292,1,0,0",
},
{
name: "invalid x coordinate",
data: "1752646457403,invalid,1574.0195,7,1.0,0.016666668,504.78418,1721.0195,924451292,924451292,1,0,0",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := ParseTouchEvents(tc.data)
if err == nil {
t.Errorf("Expected error for invalid data, but got none")
}
})
}
}
func TestTouchEventSequenceValidation(t *testing.T) {
// Test a complete touch sequence: DOWN -> MOVE -> MOVE -> UP
sequenceData := `1752646457403,100.0,100.0,7,1.0,0.016666668,100.0,100.0,924451292,924451292,1,0,0
1752646457420,120.0,120.0,7,1.0,0.022058824,120.0,120.0,924451292,924451335,1,0,2
1752646457440,140.0,140.0,7,1.0,0.022058824,140.0,140.0,924451292,924451351,1,0,2
1752646457460,160.0,160.0,7,1.0,0.012254903,160.0,160.0,924451292,924451619,1,0,1`
events, err := ParseTouchEvents(sequenceData)
if err != nil {
t.Fatalf("ParseTouchEvents failed: %v", err)
}
if len(events) != 4 {
t.Fatalf("Expected 4 events, got %d", len(events))
}
// Validate sequence: DOWN -> MOVE -> MOVE -> UP
expectedActions := []int{0, 2, 2, 1} // ACTION_DOWN, ACTION_MOVE, ACTION_MOVE, ACTION_UP
for i, event := range events {
if event.Action != expectedActions[i] {
t.Errorf("Event %d: expected action %d, got %d", i, expectedActions[i], event.Action)
}
}
t.Logf("Touch sequence validation passed: %d events with correct action sequence", len(events))
}
func TestSwipeWithDirection(t *testing.T) {
device, err := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := 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 := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := 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 := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := 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 := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := 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 := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := 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 := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := 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 := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := 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 := NewAndroidDevice(
option.WithSerialNumber(""),
)
if err != nil {
t.Fatal(err)
}
driver, err := 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)
}
})
}
}