Merge branch 'session_refactor' into 'master'

Session refactor

See merge request iesqa/httprunner!128
This commit is contained in:
余泓铮
2025-07-17 06:12:57 +00:00
9 changed files with 448 additions and 4 deletions

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
@@ -14,19 +15,41 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/internal/builtin"
)
// WingsService implements ILLMService interface using external Wings API
type WingsService struct {
apiURL string
bizId string
apiURL string
bizId string
isExternal bool
accessKey string
secretKey string
}
// NewWingsService creates a new Wings service instance
func NewWingsService() ILLMService {
// Check for environment variables for external API access
accessKey := ""
secretKey := ""
isExternal := false
apiURL := "https://vedem-algorithm.bytedance.net/algorithm/StepActionDecision"
// If environment variables are set, use external API with authentication
if ak, sk := os.Getenv("VEDEM_WINGS_AK"), os.Getenv("VEDEM_WINGS_SK"); ak != "" && sk != "" {
accessKey = ak
secretKey = sk
isExternal = true
apiURL = "https://vedem-algorithm.zijieapi.com/algorithm/StepActionDecision"
}
return &WingsService{
apiURL: "https://vedem-algorithm.bytedance.net/algorithm/StepActionDecision",
bizId: "489fdae44de048e0922a32834ea668af",
apiURL: apiURL,
bizId: "489fdae44de048e0922a32834ea668af",
isExternal: isExternal,
accessKey: accessKey,
secretKey: secretKey,
}
}
@@ -384,6 +407,16 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
// Add authentication headers if using external API
if w.isExternal {
signToken := "UNSIGNED-PAYLOAD"
token := builtin.Sign("auth-v2", w.accessKey, w.secretKey, []byte(signToken))
httpReq.Header.Add("Agw-Auth", token)
httpReq.Header.Add("Agw-Auth-Content", signToken)
httpReq.Header.Add("Content-Type", "application/json")
}
// Execute HTTP request
client := &http.Client{
Timeout: 30 * time.Second,

View File

@@ -114,6 +114,14 @@ func (dev *AndroidDevice) Setup() error {
return nil
}
func (dev *AndroidDevice) IsHealthy() (bool, error) {
state, err := dev.Device.State()
if err != nil {
return false, err
}
return state == gadb.StateOnline, nil
}
func (dev *AndroidDevice) Teardown() error {
return nil
}

View File

@@ -428,6 +428,131 @@ func (ud *UIA2Driver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Actio
return err
}
// TouchByEvents performs a complex swipe using a sequence of touch events with pressure and size data
func (ud *UIA2Driver) TouchByEvents(events []types.TouchEvent, opts ...option.ActionOption) error {
log.Info().Int("eventCount", len(events)).Msg("UIA2Driver.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(ud, option.ACTION_SwipeCoordinate, actionOptions,
startX, startY, endX, endY)
if err != nil {
return err
}
defer postHandler(ud, 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...)
urlStr := fmt.Sprintf("/session/%s/actions/swipe", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
return err
}
func (ud *UIA2Driver) SetPasteboard(contentType types.PasteboardType, content string) (err error) {
log.Info().Str("contentType", string(contentType)).
Str("content", content).Msg("UIA2Driver.SetPasteboard")

View File

@@ -45,6 +45,10 @@ func (dev *BrowserDevice) Setup() error {
return nil
}
func (dev *BrowserDevice) IsHealthy() (bool, error) {
return true, nil
}
func (dev *BrowserDevice) LogEnabled() bool {
return dev.Options.LogOn
}

View File

@@ -12,6 +12,8 @@ type IDevice interface {
UUID() string
NewDriver() (driver IDriver, err error)
IsHealthy() (bool, error)
Setup() error
Teardown() error

View File

@@ -75,6 +75,10 @@ func (dev *HarmonyDevice) Setup() error {
return nil
}
func (dev *HarmonyDevice) IsHealthy() (bool, error) {
return true, nil
}
func (dev *HarmonyDevice) Teardown() error {
return nil
}

View File

@@ -184,6 +184,18 @@ func (dev *IOSDevice) Setup() error {
return nil
}
func (dev *IOSDevice) IsHealthy() (bool, error) {
startTimestamp := time.Now()
lockdown, err := ios.ConnectLockdownWithSession(dev.DeviceEntry)
if err != nil {
return false, err
}
defer lockdown.Close()
elapsed := time.Since(startTimestamp)
log.Info().Dur("elapsed", elapsed).Msg("connect lockdown")
return true, nil
}
func (dev *IOSDevice) Teardown() error {
for _, listener := range dev.listeners {
_ = listener.Close()

View File

@@ -225,3 +225,19 @@ const (
DirectionLeft Direction = "left"
DirectionRight Direction = "right"
)
// TouchEvent represents a single touch event with all its properties
type TouchEvent struct {
X float64 `json:"x"`
Y float64 `json:"y"`
DeviceID int `json:"deviceId"`
Pressure float64 `json:"pressure"`
Size float64 `json:"size"`
RawX float64 `json:"rawX"`
RawY float64 `json:"rawY"`
DownTime int64 `json:"downTime"`
EventTime int64 `json:"eventTime"`
ToolType int `json:"toolType"`
Flag int `json:"flag"`
Action int `json:"action"`
}