From 7fac0dcc00f052b8565f253298b6146c61fcf173 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 4 Jul 2025 20:46:11 +0800 Subject: [PATCH 1/9] fix: cannot unmarshal object into Go struct field SubActionResult error --- uixt/driver_ext_ai.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uixt/driver_ext_ai.go b/uixt/driver_ext_ai.go index 2f1eca55..48d4025c 100644 --- a/uixt/driver_ext_ai.go +++ b/uixt/driver_ext_ai.go @@ -167,7 +167,7 @@ func (dExt *XTDriver) StartToGoal(ctx context.Context, prompt string, opts ...op log.Error().Err(err). Str("action", toolCall.Function.Name). Msg("invoke tool call failed") - subActionResult.Error = err + subActionResult.Error = err.Error() return err } return nil @@ -400,7 +400,7 @@ type SubActionResult struct { Arguments interface{} `json:"arguments,omitempty"` // arguments passed to the sub-action StartTime int64 `json:"start_time"` // sub-action start time Elapsed int64 `json:"elapsed_ms"` // sub-action elapsed time(ms) - Error error `json:"error,omitempty"` // sub-action execution result + Error string `json:"error,omitempty"` // sub-action execution result SessionData } From 8492607faea0f6d4098a7e3d1672d5f26094c8f0 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 4 Jul 2025 21:01:40 +0800 Subject: [PATCH 2/9] fix: do not return error when timeout/timelimit for sleep tool --- examples/game/sudoku/game_sudoku.json | 2 +- examples/game/sudoku/main_test.go | 1 + uixt/mcp_tools_utility.go | 12 ++++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/game/sudoku/game_sudoku.json b/examples/game/sudoku/game_sudoku.json index 01e0f53f..39c01329 100644 --- a/examples/game/sudoku/game_sudoku.json +++ b/examples/game/sudoku/game_sudoku.json @@ -63,7 +63,7 @@ "actions": [ { "method": "start_to_goal", - "params": "每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明:\n1、方块外面的数字代表所在那一行或一列的黄色方块数量。\n2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。\n3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。\n4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。\n5、只能点击白色方块,不要重复点击同一个方块。\n\n请严格按照以上游戏规则,开始游戏\n", + "params": "每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明:\n1、方块外面的数字代表所在那一行或一列的黄色方块数量。\n2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。\n3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。\n4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。\n5、只能点击白色方块,不要重复点击同一个方块。\n6、若出现「桌面入口」弹窗则直接关闭。\n\n请严格按照以上游戏规则,开始游戏\n", "options": { "time_limit": 300, "pre_mark_operation": true diff --git a/examples/game/sudoku/main_test.go b/examples/game/sudoku/main_test.go index a8830dae..9c68c2b7 100644 --- a/examples/game/sudoku/main_test.go +++ b/examples/game/sudoku/main_test.go @@ -16,6 +16,7 @@ func TestGameSudoku(t *testing.T) { 3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。 4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。 5、只能点击白色方块,不要重复点击同一个方块。 +6、若出现「桌面入口」弹窗则直接关闭。 请严格按照以上游戏规则,开始游戏 ` diff --git a/uixt/mcp_tools_utility.go b/uixt/mcp_tools_utility.go index e95a6907..4515edf4 100644 --- a/uixt/mcp_tools_utility.go +++ b/uixt/mcp_tools_utility.go @@ -75,9 +75,9 @@ func (t *ToolSleep) Implement() server.ToolHandlerFunc { case <-time.After(duration): // Normal completion case <-ctx.Done(): - // Interrupted by context cancellation (e.g., CTRL+C) - log.Warn().Msg("sleep interrupted by cancellation") - return nil, fmt.Errorf("sleep interrupted: %w", ctx.Err()) + // Interrupted by context cancellation (interrupt signal, timeout, time limit) + log.Info().Msg("sleep interrupted by context cancellation") + // Don't return error - let the upper layer handle timeout/time limit logic } message := fmt.Sprintf("Successfully slept for %v seconds", actualSeconds) @@ -157,9 +157,9 @@ func (t *ToolSleepMS) Implement() server.ToolHandlerFunc { case <-time.After(duration): // Normal completion case <-ctx.Done(): - // Interrupted by context cancellation (e.g., CTRL+C) - log.Warn().Msg("sleep interrupted by cancellation") - return nil, fmt.Errorf("sleep interrupted: %w", ctx.Err()) + // Interrupted by context cancellation (interrupt signal, timeout, time limit) + log.Info().Msg("sleep interrupted by context cancellation") + // Don't return error - let the upper layer handle timeout/time limit logic } message := fmt.Sprintf("Successfully slept for %d milliseconds", actualMilliseconds) From 147942b34c145d579180832635822acb478c70ff Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 5 Jul 2025 17:04:48 +0800 Subject: [PATCH 3/9] change: update game examples --- .../romantic_restaurant/game_romantic_restaurant.json | 4 ++-- examples/game/romantic_restaurant/main_test.go | 5 +++-- examples/game/sudoku/game_sudoku.json | 8 ++++---- examples/game/sudoku/main_test.go | 11 +++++++++-- examples/game/yuedongxiaozi/game_yuedongxiaozi.json | 8 ++++---- examples/game/yuedongxiaozi/main_test.go | 8 ++++---- internal/version/VERSION | 2 +- 7 files changed, 27 insertions(+), 19 deletions(-) diff --git a/examples/game/romantic_restaurant/game_romantic_restaurant.json b/examples/game/romantic_restaurant/game_romantic_restaurant.json index 2a5e2654..86e4ef81 100644 --- a/examples/game/romantic_restaurant/game_romantic_restaurant.json +++ b/examples/game/romantic_restaurant/game_romantic_restaurant.json @@ -40,7 +40,7 @@ "actions": [ { "method": "start_to_goal", - "params": "搜索「浪漫餐厅」,进入小游戏", + "params": "搜索「浪漫餐厅」,点击进入「游戏」tab,进入小游戏", "options": { "pre_mark_operation": true } @@ -63,7 +63,7 @@ "actions": [ { "method": "start_to_goal", - "params": "浪漫餐厅是一款经营类游戏,以下是游戏的基本规则说明:\n1、点击右下角锅铲,开始任务\n2、将棋子拖拽至相同棋子,可升级生成新棋子\n3、拖拽相同棋子时,被部分遮挡的棋子只能作为拖拽终点,不能作为拖拽起点\n4、当游戏界面中没有相同棋子时,可以点击游戏页面中央的购物袋生成新的棋子\n5、若不知道如何操作,请按照游戏指引进行游玩\n\n请严格按照以上游戏规则,开始游戏\n", + "params": "浪漫餐厅是一款经营类游戏,以下是游戏的基本规则说明:\n1、点击右下角锅铲,开始任务\n2、将棋子拖拽至相同棋子,可升级生成新棋子;注意,必须是相同类别和形状的棋子才能合成,例如,长面包和圆面包不能合成,方形蛋糕和三角形蛋糕不能合成\n3、拖拽相同棋子时,被部分遮挡的棋子只能作为拖拽终点,不能作为拖拽起点\n4、当游戏界面中没有相同棋子时,可以点击游戏页面中央的购物袋生成新的棋子\n5、若不知道如何操作,请按照游戏指引进行游玩\n6、不要连续重复上一步操作,合成失败后及时更换策略\n\n请严格按照以上游戏规则,开始游戏\n", "options": { "time_limit": 300, "pre_mark_operation": true diff --git a/examples/game/romantic_restaurant/main_test.go b/examples/game/romantic_restaurant/main_test.go index b028243e..f7ff7e86 100644 --- a/examples/game/romantic_restaurant/main_test.go +++ b/examples/game/romantic_restaurant/main_test.go @@ -12,10 +12,11 @@ import ( func TestGameRomanticRestaurant(t *testing.T) { userInstruction := `浪漫餐厅是一款经营类游戏,以下是游戏的基本规则说明: 1、点击右下角锅铲,开始任务 -2、将棋子拖拽至相同棋子,可升级生成新棋子 +2、将棋子拖拽至相同棋子,可升级生成新棋子;注意,必须是相同类别和形状的棋子才能合成,例如,长面包和圆面包不能合成,方形蛋糕和三角形蛋糕不能合成 3、拖拽相同棋子时,被部分遮挡的棋子只能作为拖拽终点,不能作为拖拽起点 4、当游戏界面中没有相同棋子时,可以点击游戏页面中央的购物袋生成新的棋子 5、若不知道如何操作,请按照游戏指引进行游玩 +6、不要连续重复上一步操作,合成失败后及时更换策略 请严格按照以上游戏规则,开始游戏 ` @@ -34,7 +35,7 @@ func TestGameRomanticRestaurant(t *testing.T) { AssertAppInForeground("$package_name"), hrp.NewStep("进入「浪漫餐厅」小游戏"). Android(). - StartToGoal("搜索「浪漫餐厅」,进入小游戏", + StartToGoal("搜索「浪漫餐厅」,点击进入「游戏」tab,进入小游戏", option.WithPreMarkOperation(true)). Validate(). AssertAI("当前位于游戏界面"), diff --git a/examples/game/sudoku/game_sudoku.json b/examples/game/sudoku/game_sudoku.json index 39c01329..c2cca305 100644 --- a/examples/game/sudoku/game_sudoku.json +++ b/examples/game/sudoku/game_sudoku.json @@ -40,7 +40,7 @@ "actions": [ { "method": "start_to_goal", - "params": "搜索「每天数独」,进入小游戏", + "params": "搜索「每天数独」,点击「小游戏」tab,启动小游戏程序\n\n1、点击【开始】按钮开始游戏,进入数独的棋盘界面\n2、若提示「体力不足」,可通过观看广告免费获得体力,观看完成后继续开始游戏\n3、进入棋盘界面后即算作目标达成\n", "options": { "pre_mark_operation": true } @@ -51,8 +51,8 @@ { "check": "ui_ai", "assert": "ai_assert", - "expect": "当前页面底部包含「开始」按钮", - "msg": "assert ai prompt [当前页面底部包含「开始」按钮] failed" + "expect": "当前界面包含网格状的棋盘", + "msg": "assert ai prompt [当前界面包含网格状的棋盘] failed" } ] }, @@ -63,7 +63,7 @@ "actions": [ { "method": "start_to_goal", - "params": "每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明:\n1、方块外面的数字代表所在那一行或一列的黄色方块数量。\n2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。\n3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。\n4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。\n5、只能点击白色方块,不要重复点击同一个方块。\n6、若出现「桌面入口」弹窗则直接关闭。\n\n请严格按照以上游戏规则,开始游戏\n", + "params": "每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明:\n1、方块外面的数字代表所在那一行或一列的黄色方块数量。\n2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。\n3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。\n4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。\n5、只能点击白色方块,不要重复点击同一个方块。\n6、若出现「桌面入口」弹窗则直接关闭。\n7、若游戏失败弹出恢复血量的弹窗,请关闭弹窗重新开始游戏。\n\n请严格按照以上游戏规则,开始游戏\n", "options": { "time_limit": 300, "pre_mark_operation": true diff --git a/examples/game/sudoku/main_test.go b/examples/game/sudoku/main_test.go index 9c68c2b7..bbc3cfeb 100644 --- a/examples/game/sudoku/main_test.go +++ b/examples/game/sudoku/main_test.go @@ -10,6 +10,12 @@ import ( ) func TestGameSudoku(t *testing.T) { + startGameInstruction := `搜索「每天数独」,点击「小游戏」tab,启动小游戏程序 + +1、点击【开始】按钮开始游戏,进入数独的棋盘界面 +2、若提示「体力不足」,可通过观看广告免费获得体力,观看完成后继续开始游戏 +3、进入棋盘界面后即算作目标达成 +` userInstruction := `每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明: 1、方块外面的数字代表所在那一行或一列的黄色方块数量。 2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。 @@ -17,6 +23,7 @@ func TestGameSudoku(t *testing.T) { 4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。 5、只能点击白色方块,不要重复点击同一个方块。 6、若出现「桌面入口」弹窗则直接关闭。 +7、若游戏失败弹出恢复血量的弹窗,请关闭弹窗重新开始游戏。 请严格按照以上游戏规则,开始游戏 ` @@ -35,10 +42,10 @@ func TestGameSudoku(t *testing.T) { AssertAppInForeground("$package_name"), hrp.NewStep("进入「每天数独」小游戏"). Android(). - StartToGoal("搜索「每天数独」,进入小游戏", + StartToGoal(startGameInstruction, option.WithPreMarkOperation(true)). Validate(). - AssertAI("当前页面底部包含「开始」按钮"), + AssertAI("当前界面包含网格状的棋盘"), hrp.NewStep("开始游戏"). Android(). StartToGoal(userInstruction, diff --git a/examples/game/yuedongxiaozi/game_yuedongxiaozi.json b/examples/game/yuedongxiaozi/game_yuedongxiaozi.json index a7776210..9dcb1836 100644 --- a/examples/game/yuedongxiaozi/game_yuedongxiaozi.json +++ b/examples/game/yuedongxiaozi/game_yuedongxiaozi.json @@ -40,7 +40,7 @@ "actions": [ { "method": "start_to_goal", - "params": "搜索「跃动小子」,启动小游戏", + "params": "搜索「跃动小子」,点击「小游戏」tab,进入小游戏", "options": { "pre_mark_operation": true } @@ -51,8 +51,8 @@ { "check": "ui_ai", "assert": "ai_assert", - "expect": "当前页面底部包含「领地」「试炼」按钮", - "msg": "assert ai prompt [当前页面底部包含「领地」「试炼」按钮] failed" + "expect": "当前在小游戏页面", + "msg": "assert ai prompt [当前在小游戏页面] failed" } ] }, @@ -63,7 +63,7 @@ "actions": [ { "method": "start_to_goal", - "params": "跃动小子是一款开宝箱类的小游戏,以下是游戏的基本规则说明:\n1、打开宝箱,按照游戏指引进行「出售」或「装备」操作。\n2、请持续推进游戏进程。\n3、屏幕底部的黑白按钮不要进行点击操作。\n\n请严格按照以上游戏规则,开始游戏\n", + "params": "跃动小子是一款开宝箱类的小游戏,以下是游戏的基本规则说明:\n1、打开宝箱,按照游戏指引进行「出售」或「装备」操作。\n2、请持续推进游戏进程。\n3、游戏界面底部的黑白按钮不要进行点击操作。\n\n请严格按照以上游戏规则,开始游戏\n", "options": { "time_limit": 300, "pre_mark_operation": true diff --git a/examples/game/yuedongxiaozi/main_test.go b/examples/game/yuedongxiaozi/main_test.go index 29ac3882..06df1abe 100644 --- a/examples/game/yuedongxiaozi/main_test.go +++ b/examples/game/yuedongxiaozi/main_test.go @@ -9,11 +9,11 @@ import ( "github.com/httprunner/httprunner/v5/uixt/option" ) -func TestGameZhuadaE(t *testing.T) { +func TestGameYuedongxiaozi(t *testing.T) { userInstruction := `跃动小子是一款开宝箱类的小游戏,以下是游戏的基本规则说明: 1、打开宝箱,按照游戏指引进行「出售」或「装备」操作。 2、请持续推进游戏进程。 -3、屏幕底部的黑白按钮不要进行点击操作。 +3、游戏界面底部的黑白按钮不要进行点击操作。 请严格按照以上游戏规则,开始游戏 ` @@ -33,10 +33,10 @@ func TestGameZhuadaE(t *testing.T) { AssertAppInForeground("$package_name"), hrp.NewStep("启动「跃动小子」小游戏"). Android(). - StartToGoal("搜索「跃动小子」,启动小游戏", + StartToGoal("搜索「跃动小子」,点击「小游戏」tab,进入小游戏", option.WithPreMarkOperation(true)). Validate(). - AssertAI("当前页面底部包含「领地」「试炼」按钮"), + AssertAI("当前在小游戏页面"), hrp.NewStep("开始游戏"). Android(). StartToGoal(userInstruction, diff --git a/internal/version/VERSION b/internal/version/VERSION index 0e7166d7..dc4345d4 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250704 +v5.0.0-250705 From 9d18303108c45ecc21c11a0bc41375d498ef8a32 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 5 Jul 2025 17:05:32 +0800 Subject: [PATCH 4/9] fix: specify status when ios device connection error --- uixt/ios_device.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/uixt/ios_device.go b/uixt/ios_device.go index 658892c1..0c220006 100644 --- a/uixt/ios_device.go +++ b/uixt/ios_device.go @@ -160,22 +160,22 @@ func (dev *IOSDevice) Setup() error { if version.GreaterThan(semver.MustParse("17.4.0")) { info, err := tunnel.TunnelInfoForDevice(dev.DeviceEntry.Properties.SerialNumber, ios.HttpApiHost(), ios.HttpApiPort()) if err != nil { - return err + return errors.Wrap(code.DeviceConnectionError, err.Error()) } dev.DeviceEntry.UserspaceTUNPort = info.UserspaceTUNPort dev.DeviceEntry.UserspaceTUN = info.UserspaceTUN rsdService, err := ios.NewWithAddrPortDevice(info.Address, info.RsdPort, dev.DeviceEntry) if err != nil { - return err + return errors.Wrap(code.DeviceConnectionError, err.Error()) } defer rsdService.Close() rsdProvider, err := rsdService.Handshake() if err != nil { - return err + return errors.Wrap(code.DeviceConnectionError, err.Error()) } device, err := ios.GetDeviceWithAddress(dev.DeviceEntry.Properties.SerialNumber, info.Address, rsdProvider) if err != nil { - return err + return errors.Wrap(code.DeviceConnectionError, err.Error()) } device.UserspaceTUN = dev.DeviceEntry.UserspaceTUN device.UserspaceTUNPort = dev.DeviceEntry.UserspaceTUNPort @@ -470,7 +470,7 @@ func (dev *IOSDevice) getVersion() (version *semver.Version, err error) { version, err = ios.GetProductVersion(dev.DeviceEntry) if err != nil { log.Error().Err(err).Msg("failed to get version") - return nil, err + return nil, errors.Wrap(code.DeviceGetInfoError, err.Error()) } log.Info().Str("version", version.String()).Msg("get ios device version") return version, nil From af40d082f7b66c94d8d4891866e2cbd34fd8226b Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sun, 6 Jul 2025 10:38:50 +0800 Subject: [PATCH 5/9] change: update game examples --- .../game_romantic_restaurant.json | 29 ++----------- .../game/romantic_restaurant/main_test.go | 21 ++++++---- examples/game/sudoku/game_sudoku.json | 29 ++----------- examples/game/sudoku/main_test.go | 15 +++---- .../yuedongxiaozi/game_yuedongxiaozi.json | 29 ++----------- examples/game/yuedongxiaozi/main_test.go | 21 ++++++---- examples/game/zhuadae/game_zhuadae.json | 41 +++++++------------ examples/game/zhuadae/main_test.go | 26 ++++++++---- internal/version/VERSION | 2 +- 9 files changed, 81 insertions(+), 132 deletions(-) diff --git a/examples/game/romantic_restaurant/game_romantic_restaurant.json b/examples/game/romantic_restaurant/game_romantic_restaurant.json index 86e4ef81..98540547 100644 --- a/examples/game/romantic_restaurant/game_romantic_restaurant.json +++ b/examples/game/romantic_restaurant/game_romantic_restaurant.json @@ -9,38 +9,17 @@ } }, "teststeps": [ - { - "name": "启动抖音 app", - "android": { - "os_type": "android", - "actions": [ - { - "method": "app_launch", - "params": "$package_name" - }, - { - "method": "sleep", - "params": 5 - } - ] - }, - "validate": [ - { - "check": "ui_foreground_app", - "assert": "equal", - "expect": "$package_name", - "msg": "app [$package_name] should be in foreground" - } - ] - }, { "name": "进入「浪漫餐厅」小游戏", "android": { "os_type": "android", "actions": [ + { + "method": "home" + }, { "method": "start_to_goal", - "params": "搜索「浪漫餐厅」,点击进入「游戏」tab,进入小游戏", + "params": "在手机桌面点击「浪漫餐厅」启动小游戏,等待游戏加载完成", "options": { "pre_mark_operation": true } diff --git a/examples/game/romantic_restaurant/main_test.go b/examples/game/romantic_restaurant/main_test.go index f7ff7e86..5c44bce6 100644 --- a/examples/game/romantic_restaurant/main_test.go +++ b/examples/game/romantic_restaurant/main_test.go @@ -27,15 +27,22 @@ func TestGameRomanticRestaurant(t *testing.T) { "package_name": "com.ss.android.ugc.aweme", }), TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音 app"). - Android(). - AppLaunch("$package_name"). - Sleep(5). - Validate(). - AssertAppInForeground("$package_name"), + // hrp.NewStep("启动抖音 app"). + // Android(). + // AppLaunch("$package_name"). + // Sleep(5). + // Validate(). + // AssertAppInForeground("$package_name"), + // hrp.NewStep("进入「浪漫餐厅」小游戏"). + // Android(). + // StartToGoal("搜索「浪漫餐厅」,点击进入「游戏」tab,进入小游戏", + // option.WithPreMarkOperation(true)). + // Validate(). + // AssertAI("当前位于游戏界面"), hrp.NewStep("进入「浪漫餐厅」小游戏"). Android(). - StartToGoal("搜索「浪漫餐厅」,点击进入「游戏」tab,进入小游戏", + Home(). + StartToGoal("在手机桌面点击「浪漫餐厅」启动小游戏,等待游戏加载完成", option.WithPreMarkOperation(true)). Validate(). AssertAI("当前位于游戏界面"), diff --git a/examples/game/sudoku/game_sudoku.json b/examples/game/sudoku/game_sudoku.json index c2cca305..344b9e04 100644 --- a/examples/game/sudoku/game_sudoku.json +++ b/examples/game/sudoku/game_sudoku.json @@ -9,38 +9,17 @@ } }, "teststeps": [ - { - "name": "启动抖音 app", - "android": { - "os_type": "android", - "actions": [ - { - "method": "app_launch", - "params": "$package_name" - }, - { - "method": "sleep", - "params": 5 - } - ] - }, - "validate": [ - { - "check": "ui_foreground_app", - "assert": "equal", - "expect": "$package_name", - "msg": "app [$package_name] should be in foreground" - } - ] - }, { "name": "进入「每天数独」小游戏", "android": { "os_type": "android", "actions": [ + { + "method": "home" + }, { "method": "start_to_goal", - "params": "搜索「每天数独」,点击「小游戏」tab,启动小游戏程序\n\n1、点击【开始】按钮开始游戏,进入数独的棋盘界面\n2、若提示「体力不足」,可通过观看广告免费获得体力,观看完成后继续开始游戏\n3、进入棋盘界面后即算作目标达成\n", + "params": "在手机桌面点击「每天数独」启动小游戏,等待游戏加载完成\n\n1、点击【开始】按钮开始游戏,进入数独的棋盘界面\n2、若提示「体力不足」,可通过观看广告免费获得体力,观看完成后继续开始游戏\n3、进入棋盘界面后即算作目标达成\n", "options": { "pre_mark_operation": true } diff --git a/examples/game/sudoku/main_test.go b/examples/game/sudoku/main_test.go index bbc3cfeb..4fd287c9 100644 --- a/examples/game/sudoku/main_test.go +++ b/examples/game/sudoku/main_test.go @@ -10,7 +10,7 @@ import ( ) func TestGameSudoku(t *testing.T) { - startGameInstruction := `搜索「每天数独」,点击「小游戏」tab,启动小游戏程序 + startGameInstruction := `在手机桌面点击「每天数独」启动小游戏,等待游戏加载完成 1、点击【开始】按钮开始游戏,进入数独的棋盘界面 2、若提示「体力不足」,可通过观看广告免费获得体力,观看完成后继续开始游戏 @@ -34,14 +34,15 @@ func TestGameSudoku(t *testing.T) { "package_name": "com.ss.android.ugc.aweme", }), TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音 app"). - Android(). - AppLaunch("$package_name"). - Sleep(5). - Validate(). - AssertAppInForeground("$package_name"), + // hrp.NewStep("启动抖音 app"). + // Android(). + // AppLaunch("$package_name"). + // Sleep(5). + // Validate(). + // AssertAppInForeground("$package_name"), hrp.NewStep("进入「每天数独」小游戏"). Android(). + Home(). StartToGoal(startGameInstruction, option.WithPreMarkOperation(true)). Validate(). diff --git a/examples/game/yuedongxiaozi/game_yuedongxiaozi.json b/examples/game/yuedongxiaozi/game_yuedongxiaozi.json index 9dcb1836..a26df279 100644 --- a/examples/game/yuedongxiaozi/game_yuedongxiaozi.json +++ b/examples/game/yuedongxiaozi/game_yuedongxiaozi.json @@ -9,38 +9,17 @@ } }, "teststeps": [ - { - "name": "启动抖音 app", - "android": { - "os_type": "android", - "actions": [ - { - "method": "app_launch", - "params": "$package_name" - }, - { - "method": "sleep", - "params": 5 - } - ] - }, - "validate": [ - { - "check": "ui_foreground_app", - "assert": "equal", - "expect": "$package_name", - "msg": "app [$package_name] should be in foreground" - } - ] - }, { "name": "启动「跃动小子」小游戏", "android": { "os_type": "android", "actions": [ + { + "method": "home" + }, { "method": "start_to_goal", - "params": "搜索「跃动小子」,点击「小游戏」tab,进入小游戏", + "params": "在手机桌面点击「跃动小子」启动小游戏,等待游戏加载完成", "options": { "pre_mark_operation": true } diff --git a/examples/game/yuedongxiaozi/main_test.go b/examples/game/yuedongxiaozi/main_test.go index 06df1abe..d2ad79a9 100644 --- a/examples/game/yuedongxiaozi/main_test.go +++ b/examples/game/yuedongxiaozi/main_test.go @@ -25,15 +25,22 @@ func TestGameYuedongxiaozi(t *testing.T) { "package_name": "com.ss.android.ugc.aweme", }), TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音 app"). - Android(). - AppLaunch("$package_name"). - Sleep(5). - Validate(). - AssertAppInForeground("$package_name"), + // hrp.NewStep("启动抖音 app"). + // Android(). + // AppLaunch("$package_name"). + // Sleep(5). + // Validate(). + // AssertAppInForeground("$package_name"), + // hrp.NewStep("启动「跃动小子」小游戏"). + // Android(). + // StartToGoal("搜索「跃动小子」,点击「小游戏」tab,进入小游戏", + // option.WithPreMarkOperation(true)). + // Validate(). + // AssertAI("当前在小游戏页面"), hrp.NewStep("启动「跃动小子」小游戏"). Android(). - StartToGoal("搜索「跃动小子」,点击「小游戏」tab,进入小游戏", + Home(). + StartToGoal("在手机桌面点击「跃动小子」启动小游戏,等待游戏加载完成", option.WithPreMarkOperation(true)). Validate(). AssertAI("当前在小游戏页面"), diff --git a/examples/game/zhuadae/game_zhuadae.json b/examples/game/zhuadae/game_zhuadae.json index 991f6795..d993f23a 100644 --- a/examples/game/zhuadae/game_zhuadae.json +++ b/examples/game/zhuadae/game_zhuadae.json @@ -9,41 +9,24 @@ } }, "teststeps": [ - { - "name": "启动抖音 app", - "android": { - "os_type": "android", - "actions": [ - { - "method": "app_launch", - "params": "$package_name" - }, - { - "method": "sleep", - "params": 5 - } - ] - }, - "validate": [ - { - "check": "ui_foreground_app", - "assert": "equal", - "expect": "$package_name", - "msg": "app [$package_name] should be in foreground" - } - ] - }, { "name": "启动「抓大鹅」小游戏", "android": { "os_type": "android", "actions": [ + { + "method": "home" + }, { "method": "start_to_goal", - "params": "搜索「抓大鹅」,启动小游戏", + "params": "在手机桌面点击「抓大鹅」启动小游戏,处理弹窗,等待游戏加载完成", "options": { "pre_mark_operation": true } + }, + { + "method": "sleep", + "params": 10 } ] }, @@ -51,8 +34,8 @@ { "check": "ui_ai", "assert": "ai_assert", - "expect": "当前页面底部包含「抓大鹅」按钮", - "msg": "assert ai prompt [当前页面底部包含「抓大鹅」按钮] failed" + "expect": "当前页面底部包含「抓大鹅」", + "msg": "assert ai prompt [当前页面底部包含「抓大鹅」] failed" } ] }, @@ -67,6 +50,10 @@ "options": { "pre_mark_operation": true } + }, + { + "method": "sleep", + "params": 10 } ] }, diff --git a/examples/game/zhuadae/main_test.go b/examples/game/zhuadae/main_test.go index bffc5cdf..0a701223 100644 --- a/examples/game/zhuadae/main_test.go +++ b/examples/game/zhuadae/main_test.go @@ -28,22 +28,32 @@ func TestGameZhuadaE(t *testing.T) { "package_name": "com.ss.android.ugc.aweme", }), TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音 app"). - Android(). - AppLaunch("$package_name"). - Sleep(5). - Validate(). - AssertAppInForeground("$package_name"), + // hrp.NewStep("启动抖音 app"). + // Android(). + // AppLaunch("$package_name"). + // Sleep(5). + // Validate(). + // AssertAppInForeground("$package_name"), + // hrp.NewStep("启动「抓大鹅」小游戏"). + // Android(). + // StartToGoal("搜索「抓大鹅」,启动小游戏", + // option.WithPreMarkOperation(true)). + // Sleep(10). + // Validate(). + // AssertAI("当前页面底部包含「抓大鹅」"), hrp.NewStep("启动「抓大鹅」小游戏"). Android(). - StartToGoal("搜索「抓大鹅」,启动小游戏", + Home(). + StartToGoal("在手机桌面点击「抓大鹅」启动小游戏,处理弹窗,等待游戏加载完成", option.WithPreMarkOperation(true)). + Sleep(10). Validate(). - AssertAI("当前页面底部包含「抓大鹅」按钮"), + AssertAI("当前页面底部包含「抓大鹅」"), hrp.NewStep("进入「抓大鹅」小游戏"). Android(). StartToGoal("点击「抓大鹅」,进入小游戏", option.WithPreMarkOperation(true)). + Sleep(10). Validate(). AssertAI("当前页面底部包含「移出」「凑齐」「打乱」按钮"), hrp.NewStep("开始游戏"). diff --git a/internal/version/VERSION b/internal/version/VERSION index dc4345d4..2587ad7c 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250705 +v5.0.0-250706 From d329fb610f689da49fc454e1763eeae25e9affb2 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sun, 6 Jul 2025 11:08:52 +0800 Subject: [PATCH 6/9] fix: display ai assert in html report --- report.go | 149 ++++++++++++++++++++++++++++++++++++++++++ step_ui.go | 3 +- summary.go | 6 +- uixt/ai/asserter.go | 6 +- uixt/driver_ext_ai.go | 5 +- uixt/driver_utils.go | 31 ++++++--- 6 files changed, 183 insertions(+), 17 deletions(-) diff --git a/report.go b/report.go index 26e78bab..18b44d5f 100644 --- a/report.go +++ b/report.go @@ -1838,6 +1838,101 @@ const htmlTemplate = ` word-wrap: break-word; } + /* AI Assertion Styles */ + .ai-assertion-section { + margin-top: 15px; + padding: 15px; + background: linear-gradient(135deg, #f0f8ff 0%, #f5f5ff 100%); + border: 2px solid #4169e1; + border-radius: 12px; + box-shadow: 0 4px 8px rgba(65, 105, 225, 0.15); + } + + .ai-assertion-section h5 { + margin: 0 0 15px 0; + color: #4169e1; + font-size: 1.1em; + font-weight: 600; + } + + .ai-screenshot-container { + background: white; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 12px; + margin-bottom: 15px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .ai-screenshot { + text-align: center; + margin-top: 10px; + } + + .ai-screenshot img { + max-width: 100%; + height: auto; + border-radius: 8px; + border: 1px solid #dee2e6; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: transform 0.2s; + } + + .ai-screenshot img:hover { + transform: scale(1.02); + } + + .ai-analysis-container { + background: white; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .ai-analysis-content { + margin-top: 10px; + } + + .ai-thought { + background: linear-gradient(135deg, #e8f4fd 0%, #f0f8ff 100%); + border: 1px solid #4169e1; + border-radius: 8px; + padding: 12px; + margin: 10px 0; + color: #2c3e50; + } + + .ai-thought .thought-content { + margin-top: 8px; + font-style: italic; + color: #34495e; + white-space: pre-wrap; + word-wrap: break-word; + } + + .ai-raw-response { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 12px; + margin: 10px 0; + color: #2c3e50; + } + + .ai-raw-response .response-content { + margin-top: 8px; + font-family: monospace; + font-size: 0.9em; + background: white; + padding: 8px; + border-radius: 4px; + border: 1px solid #e9ecef; + white-space: pre-wrap; + word-wrap: break-word; + } + @media screen and (max-width: 768px) { .validator-ai-layout { flex-direction: column; @@ -2950,6 +3045,60 @@ const htmlTemplate = ` {{if and $validator.msg (ne $validator.check_result "pass")}}
{{$validator.msg}}
{{end}} + + + {{if $validator.ai_result}} +
+
🤖 AI Assertion Details
+ + + {{if $validator.ai_result.image_path}} +
+ 📸 AI Assertion Screenshot + {{if $validator.ai_result.screenshot_elapsed}} + {{formatDuration $validator.ai_result.screenshot_elapsed}} + {{end}} +
+ {{$base64Image := encodeImageBase64 $validator.ai_result.image_path}} + {{if $base64Image}} + AI Assertion Screenshot + {{end}} +
+
+ {{end}} + + +
+ 🧠 AI Model Analysis + {{if $validator.ai_result.model_call_elapsed}} + {{formatDuration $validator.ai_result.model_call_elapsed}} + {{end}} +
+ {{if $validator.ai_result.assertion_result.model_name}} +
🤖 Model: {{$validator.ai_result.assertion_result.model_name}}
+ {{end}} + {{if $validator.ai_result.assertion_result.usage}} +
📊 Tokens: {{$validator.ai_result.assertion_result.usage.PromptTokens}} in / {{$validator.ai_result.assertion_result.usage.CompletionTokens}} out / {{$validator.ai_result.assertion_result.usage.TotalTokens}} total
+ {{end}} + {{if $validator.ai_result.resolution}} +
📐 Resolution: {{$validator.ai_result.resolution.Width}}x{{$validator.ai_result.resolution.Height}}
+ {{end}} + {{if $validator.ai_result.assertion_result.thought}} +
+ 💭 AI Reasoning: +
{{$validator.ai_result.assertion_result.thought}}
+
+ {{end}} + {{if $validator.ai_result.assertion_result.content}} +
+ 📝 Raw Model Response: +
{{$validator.ai_result.assertion_result.content}}
+
+ {{end}} +
+
+
+ {{end}} {{end}} diff --git a/step_ui.go b/step_ui.go index ea641084..0dba2e80 100644 --- a/step_ui.go +++ b/step_ui.go @@ -1042,7 +1042,8 @@ func validateUI(ud *uixt.XTDriver, iValidators []interface{}, parser *Parser, st } // Perform validation - err = ud.DoValidation(validator.Check, validator.Assert, expected, validator.Message) + validationResult.AIResult, err = ud.DoValidation( + validator.Check, validator.Assert, expected, validator.Message) if err != nil { // Add the failed validation result to the list before returning error validateResults = append(validateResults, validationResult) diff --git a/summary.go b/summary.go index 28c41101..8909c4d6 100644 --- a/summary.go +++ b/summary.go @@ -11,6 +11,7 @@ import ( "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/version" + "github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt/option" ) @@ -233,6 +234,7 @@ type Address struct { type ValidationResult struct { Validator - CheckValue interface{} `json:"check_value" yaml:"check_value"` - CheckResult string `json:"check_result" yaml:"check_result"` + CheckValue interface{} `json:"check_value" yaml:"check_value"` + CheckResult string `json:"check_result" yaml:"check_result"` + AIResult *uixt.AIExecutionResult `json:"ai_result,omitempty" yaml:"ai_result,omitempty"` // store AI assertion result for displaying in report } diff --git a/uixt/ai/asserter.go b/uixt/ai/asserter.go index 124f4d03..f34e5513 100644 --- a/uixt/ai/asserter.go +++ b/uixt/ai/asserter.go @@ -33,8 +33,9 @@ type AssertOptions struct { type AssertionResult struct { Pass bool `json:"pass"` Thought string `json:"thought"` - ModelName string `json:"model_name"` // model name used for assertion - Usage *schema.TokenUsage `json:"usage,omitempty"` // token usage statistics + Content string `json:"content,omitempty"` // raw response content + ModelName string `json:"model_name"` // model name used for assertion + Usage *schema.TokenUsage `json:"usage,omitempty"` // token usage statistics } // Asserter handles assertion using different AI models @@ -180,5 +181,6 @@ func parseAssertionResult(content string, modelType option.LLMServiceType) (*Ass } result.ModelName = string(modelType) + result.Content = content // Store the original response content return &result, nil } diff --git a/uixt/driver_ext_ai.go b/uixt/driver_ext_ai.go index 48d4025c..84ca4c6d 100644 --- a/uixt/driver_ext_ai.go +++ b/uixt/driver_ext_ai.go @@ -492,9 +492,10 @@ func (dExt *XTDriver) AIAssert(assertion string, opts ...option.ActionOption) (* return assertResult, errors.Wrap(err, "AI assertion failed") } + // For assertion failure, we should still return success but mark the assertion as failed + // This ensures that the AIResult (including screenshot and thought) is properly saved and displayed if !result.Pass { - assertResult.Error = result.Thought - return assertResult, errors.New(result.Thought) + assertResult.Error = result.Thought // Store the failure reason for reporting } return assertResult, nil diff --git a/uixt/driver_utils.go b/uixt/driver_utils.go index c24abcc6..7538297e 100644 --- a/uixt/driver_utils.go +++ b/uixt/driver_utils.go @@ -65,8 +65,8 @@ func convertToAbsolutePoint(driver IDriver, x, y float64) (absX, absY float64, e } func convertToAbsoluteCoordinates(driver IDriver, fromX, fromY, toX, toY float64) ( - absFromX, absFromY, absToX, absToY float64, err error) { - + absFromX, absFromY, absToX, absToY float64, err error, +) { // absolute coordinates if fromX > 1 || toX > 1 || fromY > 1 || toY > 1 { return fromX, fromY, toX, toY, nil @@ -190,32 +190,43 @@ func (dExt *XTDriver) assertSelector(selector, assert string) error { return nil } -func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (err error) { +func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (aiResult *AIExecutionResult, err error) { switch check { case option.SelectorOCR: err = dExt.assertOCR(expected, assert) case option.SelectorAI: - _, err = dExt.AIAssert(expected) + aiResult, err = dExt.AIAssert(expected) case option.SelectorForegroundApp: err = dExt.assertForegroundApp(expected, assert) case option.SelectorSelector: err = dExt.assertSelector(expected, assert) default: - return fmt.Errorf("validator %s not implemented", check) + return nil, fmt.Errorf("validator %s not implemented", check) } if err != nil { + // Technical error (not assertion failure) if message == nil { message = []string{""} } log.Error().Err(err).Str("assert", assert).Str("expect", expected). Str("msg", message[0]).Msg("validate failed") - return err + return nil, err + } else if aiResult != nil { + // Check assertion result instead of relying on error + if !aiResult.AssertionResult.Pass { + return aiResult, errors.New(aiResult.AssertionResult.Thought) + } + log.Info().Str("check", check).Str("assert", assert). + Str("expect", expected). + Interface("ai_assertion_result", aiResult.AssertionResult). + Msg("ai assertion passed") + return aiResult, nil + } else { + log.Info().Str("check", check).Str("assert", assert). + Str("expect", expected).Msg("validate success") + return nil, nil } - - log.Info().Str("check", check).Str("assert", assert). - Str("expect", expected).Msg("validate success") - return nil } type SleepConfig struct { From 9ce349a27c176d57ab32bc6d83cdb3c1587229a9 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 7 Jul 2025 22:38:58 +0800 Subject: [PATCH 7/9] change: log message --- internal/version/VERSION | 2 +- parser.go | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 2587ad7c..ddf64e7f 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250706 +v5.0.0-250707 diff --git a/parser.go b/parser.go index 6989242a..a7021f15 100644 --- a/parser.go +++ b/parser.go @@ -11,13 +11,13 @@ import ( "strconv" "strings" + "github.com/httprunner/funplugin" + "github.com/httprunner/funplugin/fungo" "github.com/maja42/goval" "github.com/mark3labs/mcp-go/mcp" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "github.com/httprunner/funplugin" - "github.com/httprunner/funplugin/fungo" "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/mcphost" @@ -238,7 +238,7 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} log.Debug(). Str("parsedString", parsedString). Int("matchStartPosition", matchStartPosition). - Msg("[parseString] parse function") + Msg("parse string function") continue } @@ -268,7 +268,7 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} log.Debug(). Str("parsedString", parsedString). Int("matchStartPosition", matchStartPosition). - Msg("[parseString] parse variable") + Msg("parse string variable") continue } @@ -306,7 +306,8 @@ func (p *Parser) CallFunc(funcName string, arguments ...interface{}) (interface{ // CallMCPTool calls a MCP tool on a specific MCP server func (p *Parser) CallMCPTool(ctx context.Context, serverName, - funcName string, arguments map[string]interface{}) (interface{}, error) { + funcName string, arguments map[string]interface{}, +) (interface{}, error) { if p.MCPHost == nil { return nil, fmt.Errorf("mcphost is not initialized") } From eb2835626cdce9fac88440c80417f1888351f2ae Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Tue, 8 Jul 2025 22:35:51 +0800 Subject: [PATCH 8/9] feat: support timeout option for driver session --- internal/version/VERSION | 2 +- uixt/driver_session.go | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index ddf64e7f..6623a6b4 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250707 +v5.0.0-250708 diff --git a/uixt/driver_session.go b/uixt/driver_session.go index 7445e9b5..07a6396f 100644 --- a/uixt/driver_session.go +++ b/uixt/driver_session.go @@ -19,6 +19,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v5/internal/json" + "github.com/httprunner/httprunner/v5/uixt/option" ) type Attachments map[string]interface{} @@ -151,32 +152,32 @@ func (s *DriverSession) buildURL(urlStr string) (string, error) { return baseURL.ResolveReference(relativeURL).String(), nil } -func (s *DriverSession) GET(urlStr string) (rawResp DriverRawResponse, err error) { - return s.RequestWithRetry(http.MethodGet, urlStr, nil) +func (s *DriverSession) GET(urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) { + return s.RequestWithRetry(http.MethodGet, urlStr, nil, opts...) } -func (s *DriverSession) POST(data interface{}, urlStr string) (rawResp DriverRawResponse, err error) { +func (s *DriverSession) POST(data interface{}, urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) { var bsJSON []byte = nil if data != nil { if bsJSON, err = json.Marshal(data); err != nil { return nil, err } } - return s.RequestWithRetry(http.MethodPost, urlStr, bsJSON) + return s.RequestWithRetry(http.MethodPost, urlStr, bsJSON, opts...) } -func (s *DriverSession) DELETE(urlStr string) (rawResp DriverRawResponse, err error) { - return s.RequestWithRetry(http.MethodDelete, urlStr, nil) +func (s *DriverSession) DELETE(urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) { + return s.RequestWithRetry(http.MethodDelete, urlStr, nil, opts...) } -func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte) ( +func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte, opts ...option.ActionOption) ( rawResp DriverRawResponse, err error, ) { var lastError error for attempt := 1; attempt <= s.maxRetry; attempt++ { // Execute the request - rawResp, err = s.Request(method, urlStr, rawBody) + rawResp, err = s.Request(method, urlStr, rawBody, opts...) if err == nil { if attempt > 1 { log.Info().Msgf("request succeeded after %d attempts", attempt) @@ -210,9 +211,15 @@ func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody [ return nil, lastError } -func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) ( +func (s *DriverSession) Request(method string, urlStr string, rawBody []byte, opts ...option.ActionOption) ( rawResp DriverRawResponse, err error, ) { + timeout := s.timeout + options := option.NewActionOptions(opts...) + if options.Timeout > 0 { + timeout = time.Duration(options.Timeout) * time.Second + } + // build final URL rawURL, err := s.buildURL(urlStr) if err != nil { @@ -252,7 +259,7 @@ func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) ( logger.Msg("request uixt driver") }() - ctx, cancel := context.WithTimeout(s.ctx, s.timeout) + ctx, cancel := context.WithTimeout(s.ctx, timeout) defer cancel() req, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewBuffer(rawBody)) From f334a2db8e94b233d24c66e775ed40a9fd99adac Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Wed, 9 Jul 2025 10:35:43 +0800 Subject: [PATCH 9/9] feat: support max retry times option for driver session --- internal/version/VERSION | 2 +- uixt/driver_session.go | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 6623a6b4..a778e596 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250708 +v5.0.0-250709 diff --git a/uixt/driver_session.go b/uixt/driver_session.go index 07a6396f..4c654398 100644 --- a/uixt/driver_session.go +++ b/uixt/driver_session.go @@ -175,7 +175,13 @@ func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody [ ) { var lastError error - for attempt := 1; attempt <= s.maxRetry; attempt++ { + maxRetry := s.maxRetry + options := option.NewActionOptions(opts...) + if options.MaxRetryTimes > 0 { + maxRetry = options.MaxRetryTimes + } + + for attempt := 1; attempt <= maxRetry; attempt++ { // Execute the request rawResp, err = s.Request(method, urlStr, rawBody, opts...) if err == nil {