mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 07:26:55 +08:00
Merge branch 'session_refactor' into 'master'
feat: 支持自主配置LLM Service See merge request iesqa/httprunner!131
This commit is contained in:
@@ -29,10 +29,6 @@ func ParseTouchEvents(data string) ([]types.TouchEvent, error) {
|
|||||||
event := types.TouchEvent{}
|
event := types.TouchEvent{}
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Parse each field
|
|
||||||
if event.Timestamp, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid timestamp: %v", err)
|
|
||||||
}
|
|
||||||
if event.X, err = strconv.ParseFloat(parts[1], 64); err != nil {
|
if event.X, err = strconv.ParseFloat(parts[1], 64); err != nil {
|
||||||
return nil, fmt.Errorf("invalid x coordinate: %v", err)
|
return nil, fmt.Errorf("invalid x coordinate: %v", err)
|
||||||
}
|
}
|
||||||
@@ -155,9 +151,6 @@ func TestTouchEventParsing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event := events[0]
|
event := events[0]
|
||||||
if event.Timestamp != 1752646457403 {
|
|
||||||
t.Errorf("Expected timestamp 1752646457403, got %d", event.Timestamp)
|
|
||||||
}
|
|
||||||
if event.X != 456.78418 {
|
if event.X != 456.78418 {
|
||||||
t.Errorf("Expected X 456.78418, got %f", event.X)
|
t.Errorf("Expected X 456.78418, got %f", event.X)
|
||||||
}
|
}
|
||||||
@@ -229,12 +222,5 @@ func TestTouchEventSequenceValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate timestamps are in increasing order
|
|
||||||
for i := 1; i < len(events); i++ {
|
|
||||||
if events[i].Timestamp <= events[i-1].Timestamp {
|
|
||||||
t.Errorf("Event %d: timestamp should be greater than previous event", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Touch sequence validation passed: %d events with correct action sequence", len(events))
|
t.Logf("Touch sequence validation passed: %d events with correct action sequence", len(events))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,22 +347,6 @@ func (w *WingsService) extractScreenshotFromMessage(message *schema.Message) (st
|
|||||||
|
|
||||||
// getDeviceInfoFromContext gets device info from context with fallback
|
// getDeviceInfoFromContext gets device info from context with fallback
|
||||||
func (w *WingsService) getDeviceInfoFromContext(ctx context.Context, screenshot string) WingsDeviceInfo {
|
func (w *WingsService) getDeviceInfoFromContext(ctx context.Context, screenshot string) WingsDeviceInfo {
|
||||||
// Try to get device info from context
|
|
||||||
if deviceID, ok := ctx.Value("device_id").(string); ok {
|
|
||||||
platformType := "android"
|
|
||||||
if platform, ok := ctx.Value("platform_type").(string); ok {
|
|
||||||
platformType = platform
|
|
||||||
}
|
|
||||||
|
|
||||||
return WingsDeviceInfo{
|
|
||||||
DeviceID: deviceID,
|
|
||||||
NowImage: screenshot,
|
|
||||||
PreImage: screenshot,
|
|
||||||
NowLayoutJSON: "",
|
|
||||||
OperationSystem: platformType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to default device info
|
// Fallback to default device info
|
||||||
return WingsDeviceInfo{
|
return WingsDeviceInfo{
|
||||||
DeviceID: "default-device",
|
DeviceID: "default-device",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func setupADBDriverExt(t *testing.T) *XTDriver {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
driverExt, err := NewXTDriver(driver,
|
driverExt, err := NewXTDriver(driver,
|
||||||
option.WithCVService(option.CVServiceTypeVEDEM),
|
option.WithCVService(option.CVServiceTypeVEDEM),
|
||||||
// option.WithLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428),
|
// option.WithLLMService(option.DOUBAO_1_5_UI_TARS_250328),
|
||||||
)
|
)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
return driverExt
|
return driverExt
|
||||||
|
|||||||
+32
-96
@@ -209,80 +209,51 @@ func (dExt *XTDriver) AIAction(ctx context.Context, prompt string, opts ...optio
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Check if WingsService is available and prioritize it
|
// Step 2: Check if WingsService is available and prioritize it
|
||||||
if dExt.WingsService != nil {
|
if dExt.LLMService != nil {
|
||||||
log.Info().Msg("using Wings service for AI action")
|
log.Info().Msg("using Wings service for AI action")
|
||||||
return dExt.executeAIAction(ctx, prompt, screenResult, dExt.WingsService, "wings", opts...)
|
return dExt.executeAIAction(ctx, prompt, screenResult, dExt.LLMService, "wings", opts...)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("no LLM service is initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Fallback to LLM service
|
|
||||||
if dExt.LLMService == nil {
|
|
||||||
return nil, errors.New("neither Wings service nor LLM service is initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msg("using LLM service for AI action")
|
|
||||||
return dExt.executeAIAction(ctx, prompt, screenResult, dExt.LLMService, "llm", opts...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeAIAction executes AIAction using any AI service (generic implementation)
|
// executeAIAction executes AIAction using any AI service (generic implementation)
|
||||||
func (dExt *XTDriver) executeAIAction(ctx context.Context, prompt string, screenResult *ScreenResult, service ai.ILLMService, serviceType string, opts ...option.ActionOption) (*AIExecutionResult, error) {
|
func (dExt *XTDriver) executeAIAction(ctx context.Context, prompt string, screenResult *ScreenResult, service ai.ILLMService, serviceType string, opts ...option.ActionOption) (*AIExecutionResult, error) {
|
||||||
// Add device context for Wings service if needed
|
|
||||||
if serviceType == "wings" {
|
|
||||||
ctx = dExt.addDeviceContextForWings(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Plan next action and measure time
|
// Step 1: Plan next action and measure time
|
||||||
modelCallStartTime := time.Now()
|
modelCallStartTime := time.Now()
|
||||||
|
|
||||||
var planningResult *ai.PlanningResult
|
var planningResult *ai.PlanningResult
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if serviceType == "llm" {
|
// For Wings service, call Plan directly
|
||||||
// For LLM service, use PlanNextAction which includes additional processing
|
planningOpts := &ai.PlanningOptions{
|
||||||
planningExecutionResult, planErr := dExt.PlanNextAction(ctx, prompt, opts...)
|
UserInstruction: prompt,
|
||||||
if planErr != nil {
|
Message: &schema.Message{
|
||||||
modelCallElapsed := time.Since(modelCallStartTime).Milliseconds()
|
Role: schema.User,
|
||||||
return &AIExecutionResult{
|
MultiContent: []schema.ChatMessagePart{
|
||||||
Type: "action",
|
{
|
||||||
ModelCallElapsed: modelCallElapsed,
|
Type: schema.ChatMessagePartTypeImageURL,
|
||||||
ScreenshotElapsed: screenResult.Elapsed,
|
ImageURL: &schema.ChatMessageImageURL{
|
||||||
ImagePath: screenResult.ImagePath,
|
URL: screenResult.Base64,
|
||||||
Resolution: &screenResult.Resolution,
|
|
||||||
Error: planErr.Error(),
|
|
||||||
}, errors.Wrap(planErr, "get next action failed")
|
|
||||||
}
|
|
||||||
planningResult = &planningExecutionResult.PlanningResult
|
|
||||||
} else {
|
|
||||||
// For Wings service, call Plan directly
|
|
||||||
planningOpts := &ai.PlanningOptions{
|
|
||||||
UserInstruction: prompt,
|
|
||||||
Message: &schema.Message{
|
|
||||||
Role: schema.User,
|
|
||||||
MultiContent: []schema.ChatMessagePart{
|
|
||||||
{
|
|
||||||
Type: schema.ChatMessagePartTypeImageURL,
|
|
||||||
ImageURL: &schema.ChatMessageImageURL{
|
|
||||||
URL: screenResult.Base64,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Size: screenResult.Resolution,
|
},
|
||||||
}
|
Size: screenResult.Resolution,
|
||||||
|
|
||||||
planningResult, err = service.Plan(ctx, planningOpts)
|
|
||||||
if err != nil {
|
|
||||||
modelCallElapsed := time.Since(modelCallStartTime).Milliseconds()
|
|
||||||
return &AIExecutionResult{
|
|
||||||
Type: "action",
|
|
||||||
ModelCallElapsed: modelCallElapsed,
|
|
||||||
ScreenshotElapsed: screenResult.Elapsed,
|
|
||||||
ImagePath: screenResult.ImagePath,
|
|
||||||
Resolution: &screenResult.Resolution,
|
|
||||||
Error: err.Error(),
|
|
||||||
}, errors.Wrap(err, fmt.Sprintf("%s service planning failed", serviceType))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
planningResult, err = service.Plan(ctx, planningOpts)
|
||||||
|
if err != nil {
|
||||||
|
modelCallElapsed := time.Since(modelCallStartTime).Milliseconds()
|
||||||
|
return &AIExecutionResult{
|
||||||
|
Type: "action",
|
||||||
|
ModelCallElapsed: modelCallElapsed,
|
||||||
|
ScreenshotElapsed: screenResult.Elapsed,
|
||||||
|
ImagePath: screenResult.ImagePath,
|
||||||
|
Resolution: &screenResult.Resolution,
|
||||||
|
Error: err.Error(),
|
||||||
|
}, errors.Wrap(err, fmt.Sprintf("%s service planning failed", serviceType))
|
||||||
|
}
|
||||||
modelCallElapsed := time.Since(modelCallStartTime).Milliseconds()
|
modelCallElapsed := time.Since(modelCallStartTime).Milliseconds()
|
||||||
|
|
||||||
aiExecutionResult := &AIExecutionResult{
|
aiExecutionResult := &AIExecutionResult{
|
||||||
@@ -322,28 +293,18 @@ func (dExt *XTDriver) AIAssert(assertion string, opts ...option.ActionOption) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Check if WingsService is available and prioritize it
|
if dExt.LLMService != nil {
|
||||||
if dExt.WingsService != nil {
|
|
||||||
log.Info().Msg("using Wings service for AI assertion")
|
log.Info().Msg("using Wings service for AI assertion")
|
||||||
return dExt.executeAIAssert(assertion, screenResult, dExt.WingsService, "wings", opts...)
|
return dExt.executeAIAssert(assertion, screenResult, dExt.LLMService, "wings", opts...)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("no LLM service is initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Fallback to LLM service
|
|
||||||
if dExt.LLMService == nil {
|
|
||||||
return nil, errors.New("neither Wings service nor LLM service is initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msg("using LLM service for AI assertion")
|
|
||||||
return dExt.executeAIAssert(assertion, screenResult, dExt.LLMService, "llm", opts...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeAIAssert executes AIAssert using any AI service (generic implementation)
|
// executeAIAssert executes AIAssert using any AI service (generic implementation)
|
||||||
func (dExt *XTDriver) executeAIAssert(assertion string, screenResult *ScreenResult, service ai.ILLMService, serviceType string, opts ...option.ActionOption) (*AIExecutionResult, error) {
|
func (dExt *XTDriver) executeAIAssert(assertion string, screenResult *ScreenResult, service ai.ILLMService, serviceType string, opts ...option.ActionOption) (*AIExecutionResult, error) {
|
||||||
// Step 1: Prepare context and options
|
// Step 1: Prepare context and options
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if serviceType == "wings" {
|
|
||||||
ctx = dExt.addDeviceContextForWings(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertResult := &AIExecutionResult{
|
assertResult := &AIExecutionResult{
|
||||||
Type: "assert",
|
Type: "assert",
|
||||||
@@ -376,31 +337,6 @@ func (dExt *XTDriver) executeAIAssert(assertion string, screenResult *ScreenResu
|
|||||||
return assertResult, nil
|
return assertResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addDeviceContextForWings adds device information to context for Wings service
|
|
||||||
func (dExt *XTDriver) addDeviceContextForWings(ctx context.Context) context.Context {
|
|
||||||
device := dExt.GetDevice()
|
|
||||||
if device == nil {
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add device ID to context
|
|
||||||
ctx = context.WithValue(ctx, "device_id", device.UUID())
|
|
||||||
|
|
||||||
// Add platform type to context
|
|
||||||
platformType := "android" // default
|
|
||||||
switch device.(type) {
|
|
||||||
case *AndroidDevice:
|
|
||||||
platformType = "android"
|
|
||||||
case *IOSDevice:
|
|
||||||
platformType = "ios"
|
|
||||||
case *HarmonyDevice:
|
|
||||||
platformType = "harmony"
|
|
||||||
}
|
|
||||||
ctx = context.WithValue(ctx, "platform_type", platformType)
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlanNextAction (original implementation - preserved)
|
// PlanNextAction (original implementation - preserved)
|
||||||
func (dExt *XTDriver) PlanNextAction(ctx context.Context, prompt string, opts ...option.ActionOption) (*PlanningExecutionResult, error) {
|
func (dExt *XTDriver) PlanNextAction(ctx context.Context, prompt string, opts ...option.ActionOption) (*PlanningExecutionResult, error) {
|
||||||
if dExt.LLMService == nil {
|
if dExt.LLMService == nil {
|
||||||
|
|||||||
@@ -295,32 +295,25 @@ func TestDriverExt_AIAction_CompareWithAIAction(t *testing.T) {
|
|||||||
prompt := "点击搜索按钮"
|
prompt := "点击搜索按钮"
|
||||||
|
|
||||||
// Test both methods with the same prompt
|
// Test both methods with the same prompt
|
||||||
wingsResult, wingsErr := driver.AIAction(context.Background(), prompt)
|
|
||||||
aiResult, aiErr := driver.AIAction(context.Background(), prompt)
|
aiResult, aiErr := driver.AIAction(context.Background(), prompt)
|
||||||
|
|
||||||
// Both should execute without critical errors (may have different implementations)
|
// Both should execute without critical errors (may have different implementations)
|
||||||
t.Logf("AIAction error: %v", wingsErr)
|
|
||||||
t.Logf("AIAction error: %v", aiErr)
|
t.Logf("AIAction error: %v", aiErr)
|
||||||
|
|
||||||
// If both succeed, compare results
|
// If both succeed, compare results
|
||||||
if wingsResult != nil && aiResult != nil {
|
if aiResult != nil {
|
||||||
assert.Equal(t, "action", wingsResult.Type, "AIAction result type should be 'action'")
|
|
||||||
assert.Equal(t, "action", aiResult.Type, "AIAction result type should be 'action'")
|
assert.Equal(t, "action", aiResult.Type, "AIAction result type should be 'action'")
|
||||||
|
|
||||||
// Both should have timing information
|
// Both should have timing information
|
||||||
assert.Greater(t, wingsResult.ModelCallElapsed, int64(0), "AIAction should have model call elapsed time")
|
|
||||||
assert.Greater(t, aiResult.ModelCallElapsed, int64(0), "AIAction should have model call elapsed time")
|
assert.Greater(t, aiResult.ModelCallElapsed, int64(0), "AIAction should have model call elapsed time")
|
||||||
|
|
||||||
// Both should have screenshot information
|
// Both should have screenshot information
|
||||||
assert.NotEmpty(t, wingsResult.ImagePath, "AIAction should have image path")
|
|
||||||
assert.NotEmpty(t, aiResult.ImagePath, "AIAction should have image path")
|
assert.NotEmpty(t, aiResult.ImagePath, "AIAction should have image path")
|
||||||
|
|
||||||
// Compare model names
|
// Compare model names
|
||||||
if wingsResult.PlanningResult != nil && aiResult.PlanningResult != nil {
|
if aiResult.PlanningResult != nil {
|
||||||
t.Logf("AIAction model: %s", wingsResult.PlanningResult.ModelName)
|
|
||||||
t.Logf("AIAction model: %s", aiResult.PlanningResult.ModelName)
|
t.Logf("AIAction model: %s", aiResult.PlanningResult.ModelName)
|
||||||
|
|
||||||
assert.Equal(t, "wings-api", wingsResult.PlanningResult.ModelName, "AIAction should use wings-api")
|
|
||||||
assert.NotEqual(t, "wings-api", aiResult.PlanningResult.ModelName, "AIAction should not use wings-api")
|
assert.NotEqual(t, "wings-api", aiResult.PlanningResult.ModelName, "AIAction should not use wings-api")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-15
@@ -27,10 +27,6 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Initialize Wings service (always available)
|
|
||||||
driverExt.WingsService = ai.NewWingsService()
|
|
||||||
log.Info().Msg("Wings service initialized")
|
|
||||||
|
|
||||||
// Handle LLM service initialization
|
// Handle LLM service initialization
|
||||||
if services.LLMConfig != nil {
|
if services.LLMConfig != nil {
|
||||||
// Use advanced LLM configuration if provided
|
// Use advanced LLM configuration if provided
|
||||||
@@ -49,17 +45,15 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
|
|||||||
log.Info().Msg("LLM service initialized")
|
log.Info().Msg("LLM service initialized")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Info().Msg("no LLM service config provided, using Wings service only")
|
driverExt.LLMService = ai.NewWingsService()
|
||||||
}
|
log.Info().Msg("Wings service initialized")
|
||||||
|
|
||||||
// Register uixt MCP tools to LLM service if it exists
|
|
||||||
mcpTools := driverExt.client.Server.ListTools()
|
|
||||||
einoTools := ai.ConvertMCPToolsToEinoToolInfos(mcpTools, "uixt")
|
|
||||||
if err = driverExt.WingsService.RegisterTools(einoTools); err != nil {
|
|
||||||
log.Debug().Err(err).Msg("Wings service ignoring tool registration (expected)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if driverExt.LLMService != nil {
|
if driverExt.LLMService != nil {
|
||||||
|
// Register uixt MCP tools to LLM service if it exists
|
||||||
|
mcpTools := driverExt.client.Server.ListTools()
|
||||||
|
einoTools := ai.ConvertMCPToolsToEinoToolInfos(mcpTools, "uixt")
|
||||||
|
|
||||||
if err = driverExt.LLMService.RegisterTools(einoTools); err != nil {
|
if err = driverExt.LLMService.RegisterTools(einoTools); err != nil {
|
||||||
log.Warn().Err(err).Msg("failed to register uixt tools to LLM service")
|
log.Warn().Err(err).Msg("failed to register uixt tools to LLM service")
|
||||||
}
|
}
|
||||||
@@ -71,9 +65,8 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
|
|||||||
// XTDriver = IDriver + AI
|
// XTDriver = IDriver + AI
|
||||||
type XTDriver struct {
|
type XTDriver struct {
|
||||||
IDriver
|
IDriver
|
||||||
CVService ai.ICVService // OCR/CV
|
CVService ai.ICVService // OCR/CV
|
||||||
LLMService ai.ILLMService // LLM (fallback service)
|
LLMService ai.ILLMService // LLM (fallback service)
|
||||||
WingsService ai.ILLMService // Wings API service (priority service)
|
|
||||||
|
|
||||||
services *option.AIServiceOptions // AI services options
|
services *option.AIServiceOptions // AI services options
|
||||||
client *MCPClient4XTDriver // MCP Client for built-in uixt server
|
client *MCPClient4XTDriver // MCP Client for built-in uixt server
|
||||||
|
|||||||
Reference in New Issue
Block a user