From ba43e9fd0eb355a909988758a07f3a0e0967dea5 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Fri, 27 Jun 2025 11:52:53 +0800 Subject: [PATCH] refactor: enhance JSON handling and improve request retry logic in DriverSession --- internal/version/VERSION | 2 +- report.go | 450 ++++++++++++++++++++++++++------ scripts/install-pre-commit-hook | 2 +- step_ui.go | 4 + uixt/driver_session.go | 35 ++- uixt/ios_driver_wda.go | 23 +- uixt/ios_test.go | 3 +- uixt/mcp_server.go | 32 ++- uixt/mcp_tools_ai.go | 5 +- uixt/mcp_tools_swipe.go | 84 ++---- uixt/mcp_tools_touch.go | 38 +-- uixt/mcp_tools_web.go | 11 +- uixt/option/action.go | 9 +- uixt/option/device.go | 11 - uixt/option/ios.go | 10 - 15 files changed, 484 insertions(+), 235 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index d3845ad3..35e28788 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0 +v5.0.0-2506271152 diff --git a/report.go b/report.go index 54f8bee0..3f304175 100644 --- a/report.go +++ b/report.go @@ -12,11 +12,12 @@ import ( "strings" "time" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt/option" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" ) // GenerateHTMLReportFromFiles is a convenience function to generate HTML report @@ -553,13 +554,14 @@ func (g *HTMLReportGenerator) GenerateReport(outputFile string) error { "safeHTML": func(s string) template.HTML { return template.HTML(s) }, - "toJSON": func(v any) string { + "toJSONFormatted": func(v any) string { var buf strings.Builder encoder := json.NewEncoder(&buf) encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") _ = encoder.Encode(v) - result := buf.String() - return strings.TrimSpace(result) + result := strings.TrimSpace(buf.String()) + return result }, "add": func(a, b int) int { return a + b }, "base": filepath.Base, @@ -584,6 +586,20 @@ func (g *HTMLReportGenerator) GenerateReport(outputFile string) error { // If not JSON or no thought field, return original content return content }, + "formatBodyContent": func(content string) string { + // Try to parse as JSON to format + var data interface{} + if err := json.Unmarshal([]byte(content), &data); err == nil { + var buf strings.Builder + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + _ = encoder.Encode(data) + return strings.TrimSpace(buf.String()) + } + // If not JSON, return original content + return content + }, } // Parse template @@ -1372,9 +1388,12 @@ const htmlTemplate = ` margin: 2px 0; font-family: monospace; font-size: 0.7em; - max-height: 60px; + max-height: 80px; overflow-y: auto; word-break: break-all; + white-space: nowrap; + overflow-x: auto; + line-height: 1.3; } .model-output-compact { @@ -1504,12 +1523,6 @@ const htmlTemplate = ` font-weight: 600; } - .screenshots-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 10px; - } - .screenshots-horizontal { display: flex; gap: 15px; @@ -1903,26 +1916,6 @@ const htmlTemplate = ` overflow: hidden; } - .controls { - text-align: center; - margin-bottom: 20px; - } - - .controls button { - background: #007bff; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - margin: 0 5px; - cursor: pointer; - transition: background-color 0.3s; - } - - .controls button:hover { - background: #0056b3; - } - /* Modal styles */ .modal { display: none; @@ -2008,6 +2001,68 @@ const htmlTemplate = ` border-radius: 0 0 12px 12px; } + /* JSON Syntax Highlighting */ + .json-key { + color: #0066cc; + font-weight: bold; + } + + .json-string { + color: #22863a; + } + + .json-number { + color: #e36209; + } + + .json-boolean { + color: #d73a49; + font-weight: bold; + } + + .json-null { + color: #6f42c1; + font-weight: bold; + } + + .json-punctuation { + color: #24292e; + } + + .json-brace { + color: #586069; + font-weight: bold; + } + + .json-bracket { + color: #586069; + font-weight: bold; + } + + /* Inline JSON highlighting for smaller displays */ + .json-inline .json-key { + color: #0066cc; + font-weight: 600; + } + + .json-inline .json-string { + color: #22863a; + } + + .json-inline .json-number { + color: #e36209; + } + + .json-inline .json-boolean { + color: #d73a49; + font-weight: 600; + } + + .json-inline .json-null { + color: #6f42c1; + font-weight: 600; + } + .json-toolbar { background: #e9ecef; padding: 10px 20px; @@ -2290,21 +2345,6 @@ const htmlTemplate = ` font-size: 0.95em; } - .action-output { - background: #f8f9fa; - border: 2px solid #6f42c1; - border-radius: 6px; - padding: 10px; - font-size: 0.85em; - max-height: 120px; - overflow-y: auto; - white-space: pre-wrap; - word-wrap: break-word; - color: #495057; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - line-height: 1.4; - } - .action-session-data { margin-top: 15px; padding: 15px; @@ -2538,7 +2578,7 @@ const htmlTemplate = `
🔧 Tool Calls: {{$planning.ToolCallsCount}}
{{end}} {{if $planning.ActionNames}} -
🎯 Actions: {{safeHTML (toJSON $planning.ActionNames)}}
+
🎯 Actions: {{toJSONFormatted $planning.ActionNames}}
{{end}} @@ -2560,7 +2600,9 @@ const htmlTemplate = ` {{if $subAction.Error}}{{else}}{{end}} {{if $subAction.Arguments}} -
{{safeHTML (toJSON $subAction.Arguments)}}
+
+ {{toJSONFormatted $subAction.Arguments}} +
{{end}} {{if $subAction.Requests}}
@@ -2668,7 +2710,7 @@ const htmlTemplate = ` {{/* Display structured data for query results */}} {{if $action.AIResult.QueryResult.Data}}
📥 Structured Data:
-
{{safeHTML (toJSON $action.AIResult.QueryResult.Data)}}
+
{{toJSONFormatted $action.AIResult.QueryResult.Data}}
{{end}} {{else if eq $action.AIResult.Type "action"}} {{if $action.AIResult.PlanningResult.ModelName}} @@ -2850,7 +2892,9 @@ const htmlTemplate = ` {{end}}
{{if $logEntry.Fields}} - + {{end}} {{end}} @@ -2958,6 +3002,121 @@ const htmlTemplate = ` const logContent = decodeBase64UTF8(logContentBase64); const caseContent = decodeBase64UTF8(caseContentBase64); + // Enhanced JSON highlighting with better parsing + function highlightJSONAdvanced(jsonString) { + if (!jsonString || typeof jsonString !== 'string') { + return jsonString; + } + + let result = ''; + let i = 0; + let inString = false; + let inKey = false; + let escaped = false; + + while (i < jsonString.length) { + const char = jsonString[i]; + const nextChar = jsonString[i + 1]; + + if (escaped) { + result += char; + escaped = false; + i++; + continue; + } + + if (char === '\\' && inString) { + escaped = true; + result += char; + i++; + continue; + } + + if (char === '"') { + if (!inString) { + // Starting a string + inString = true; + // Check if this is a key (followed by colon) + let j = i + 1; + let tempStr = ''; + let tempEscaped = false; + while (j < jsonString.length) { + const c = jsonString[j]; + if (tempEscaped) { + tempEscaped = false; + j++; + continue; + } + if (c === '\\') { + tempEscaped = true; + j++; + continue; + } + if (c === '"') { + // End of string, check what follows + j++; + while (j < jsonString.length && /\s/.test(jsonString[j])) j++; + if (j < jsonString.length && jsonString[j] === ':') { + inKey = true; + } + break; + } + j++; + } + + if (inKey) { + result += '"'; + } else { + result += '"'; + } + } else { + // Ending a string + inString = false; + result += '"'; + inKey = false; + } + } else if (!inString) { + // Handle non-string content + if (char === ':') { + result += ':'; + } else if (char === ',') { + result += ','; + } else if (char === '{' || char === '}') { + result += '' + char + ''; + } else if (char === '[' || char === ']') { + result += '' + char + ''; + } else if (/\d/.test(char) || (char === '-' && /\d/.test(nextChar))) { + // Handle numbers + let numStr = ''; + while (i < jsonString.length && /[\d\.\-\+e]/i.test(jsonString[i])) { + numStr += jsonString[i]; + i++; + } + result += '' + numStr + ''; + i--; // Adjust for the loop increment + } else if (char === 't' && jsonString.substr(i, 4) === 'true') { + result += 'true'; + i += 3; // Skip the rest of 'true' + } else if (char === 'f' && jsonString.substr(i, 5) === 'false') { + result += 'false'; + i += 4; // Skip the rest of 'false' + } else if (char === 'n' && jsonString.substr(i, 4) === 'null') { + result += 'null'; + i += 3; // Skip the rest of 'null' + } else { + result += char; + } + } else { + // Inside string, just add character + result += char; + } + + i++; + } + + return result; + } + // Download functions function downloadSummary() { if (!summaryContent) { @@ -2997,9 +3156,11 @@ const htmlTemplate = ` try { // Parse and format JSON for beautiful display const jsonObj = JSON.parse(caseContent); - const formattedJson = JSON.stringify(jsonObj, null, 2); + const formattedJson = JSON.stringify(jsonObj, null, 4); - document.getElementById('jsonContent').textContent = formattedJson; + // Apply syntax highlighting + const highlightedJson = highlightJSONAdvanced(formattedJson); + document.getElementById('jsonContent').innerHTML = highlightedJson; document.getElementById('jsonModal').style.display = 'block'; } catch (e) { console.error('Failed to parse JSON:', e); @@ -3014,22 +3175,39 @@ const htmlTemplate = ` } function copyJsonContent() { - const jsonContent = document.getElementById('jsonContent').textContent; - if (!jsonContent) { + // Copy the original formatted JSON content instead of highlighted HTML + if (!caseContent) { alert('No content to copy'); return; } - navigator.clipboard.writeText(jsonContent).then(function() { - const copyStatus = document.getElementById('copyStatus'); - copyStatus.classList.add('show'); - setTimeout(function() { - copyStatus.classList.remove('show'); - }, 2000); - }).catch(function(err) { - console.error('Failed to copy to clipboard:', err); - alert('Failed to copy to clipboard. Please select and copy manually.'); - }); + try { + const jsonObj = JSON.parse(caseContent); + const formattedJson = JSON.stringify(jsonObj, null, 4); + + navigator.clipboard.writeText(formattedJson).then(function() { + const copyStatus = document.getElementById('copyStatus'); + copyStatus.classList.add('show'); + setTimeout(function() { + copyStatus.classList.remove('show'); + }, 2000); + }).catch(function(err) { + console.error('Failed to copy to clipboard:', err); + alert('Failed to copy to clipboard. Please select and copy manually.'); + }); + } catch (e) { + // Fallback to original content + navigator.clipboard.writeText(caseContent).then(function() { + const copyStatus = document.getElementById('copyStatus'); + copyStatus.classList.add('show'); + setTimeout(function() { + copyStatus.classList.remove('show'); + }, 2000); + }).catch(function(err) { + console.error('Failed to copy to clipboard:', err); + alert('Failed to copy to clipboard. Please select and copy manually.'); + }); + } } function downloadCaseJson() { @@ -3050,9 +3228,11 @@ const htmlTemplate = ` try { // Parse and format JSON for beautiful display const jsonObj = JSON.parse(summaryContent); - const formattedJson = JSON.stringify(jsonObj, null, 2); + const formattedJson = JSON.stringify(jsonObj, null, 4); - document.getElementById('summaryContent').textContent = formattedJson; + // Apply syntax highlighting + const highlightedJson = highlightJSONAdvanced(formattedJson); + document.getElementById('summaryContent').innerHTML = highlightedJson; document.getElementById('summaryModal').style.display = 'block'; } catch (e) { console.error('Failed to parse JSON:', e); @@ -3067,22 +3247,39 @@ const htmlTemplate = ` } function copySummaryContent() { - const content = document.getElementById('summaryContent').textContent; - if (!content) { + // Copy the original formatted JSON content instead of highlighted HTML + if (!summaryContent) { alert('No content to copy'); return; } - navigator.clipboard.writeText(content).then(function() { - const copyStatus = document.getElementById('summaryStatus'); - copyStatus.classList.add('show'); - setTimeout(function() { - copyStatus.classList.remove('show'); - }, 2000); - }).catch(function(err) { - console.error('Failed to copy to clipboard:', err); - alert('Failed to copy to clipboard. Please select and copy manually.'); - }); + try { + const jsonObj = JSON.parse(summaryContent); + const formattedJson = JSON.stringify(jsonObj, null, 4); + + navigator.clipboard.writeText(formattedJson).then(function() { + const copyStatus = document.getElementById('summaryStatus'); + copyStatus.classList.add('show'); + setTimeout(function() { + copyStatus.classList.remove('show'); + }, 2000); + }).catch(function(err) { + console.error('Failed to copy to clipboard:', err); + alert('Failed to copy to clipboard. Please select and copy manually.'); + }); + } catch (e) { + // Fallback to original content + navigator.clipboard.writeText(summaryContent).then(function() { + const copyStatus = document.getElementById('summaryStatus'); + copyStatus.classList.add('show'); + setTimeout(function() { + copyStatus.classList.remove('show'); + }, 2000); + }).catch(function(err) { + console.error('Failed to copy to clipboard:', err); + alert('Failed to copy to clipboard. Please select and copy manually.'); + }); + } } // Log Content Modal functions @@ -3141,6 +3338,19 @@ const htmlTemplate = ` if (fieldsElement.classList.contains('collapsed')) { fieldsElement.classList.remove('collapsed'); toggleIcon.classList.add('rotated'); + // Apply JSON highlighting when expanding + if (fieldsElement.classList.contains('json-inline')) { + const text = fieldsElement.textContent; + if (text && text.trim()) { + try { + JSON.parse(text); + const highlighted = highlightJSONAdvanced(text); + fieldsElement.innerHTML = highlighted; + } catch (e) { + // If not valid JSON, leave as is + } + } + } } else { fieldsElement.classList.add('collapsed'); toggleIcon.classList.remove('rotated'); @@ -3173,9 +3383,64 @@ const htmlTemplate = ` } else { requestsContent.classList.add('show'); buttonElement.textContent = buttonElement.textContent.replace('Show', 'Hide'); + + // Apply JSON highlighting to request/response bodies when expanding + setTimeout(() => { + applyRequestResponseHighlighting(requestsContent); + }, 10); } } + // Apply JSON highlighting to request/response content + function applyRequestResponseHighlighting(container) { + // Find all request-body-compact and response-body-compact elements + const requestBodies = container.querySelectorAll('.request-body-compact, .response-body-compact'); + + requestBodies.forEach(function(element) { + // Skip if already processed + if (element.querySelector('.json-key, .json-string, .json-number')) { + return; + } + + const text = element.textContent; + if (text && text.trim()) { + // Extract the content after "Request:" or "Response:" + const match = text.match(/^(Request|Response):\s*(.+)$/s); + if (match) { + const label = match[1]; + const content = match[2].trim(); + try { + // Validate JSON by parsing it + const parsedJson = JSON.parse(content); + // Re-stringify to get a compact, normalized string, which removes extra spaces + const compactJson = JSON.stringify(parsedJson); + // Apply highlighting on the compact string + const highlighted = highlightJSONAdvanced(compactJson); + element.innerHTML = label + ': ' + highlighted; + } catch (e) { + // If not valid JSON, leave as is + console.log('Not valid JSON for ' + label + ':', content); + } + } else { + // Try to find JSON-like content even without exact format + const jsonMatch = text.match(/(\{.*\}|\[.*\])/s); + if (jsonMatch) { + const jsonContent = jsonMatch[1].trim(); + try { + JSON.parse(jsonContent); + const beforeJson = text.substring(0, text.indexOf(jsonContent)); + const afterJson = text.substring(text.indexOf(jsonContent) + jsonContent.length); + const highlighted = highlightJSONAdvanced(jsonContent); + element.innerHTML = beforeJson + highlighted + afterJson; + } catch (e) { + // Not valid JSON, leave as is + } + } + } + } + }); + } + function openImageModal(src) { const modal = document.getElementById('imageModal'); const modalImg = document.getElementById('modalImage'); @@ -3208,6 +3473,25 @@ const htmlTemplate = ` } } + // Apply syntax highlighting to inline JSON content + function applyInlineJSONHighlighting() { + document.querySelectorAll('.json-inline').forEach(function(element) { + const text = element.textContent; + if (text && text.trim()) { + try { + // Validate and parse JSON + JSON.parse(text); + // Apply highlighting if valid JSON + const highlighted = highlightJSONAdvanced(text); + element.innerHTML = highlighted; + } catch (e) { + // If not valid JSON, leave as is + // This handles cases where content might not be pure JSON + } + } + }); + } + // Auto-expand all steps on load to show actions document.addEventListener('DOMContentLoaded', function() { // Expand all steps to show the actions list @@ -3216,6 +3500,14 @@ const htmlTemplate = ` contents.forEach(content => content.classList.add('show')); icons.forEach(icon => icon.classList.add('rotated')); + + // Apply syntax highlighting to inline JSON content + applyInlineJSONHighlighting(); + + // Apply JSON highlighting to all visible request/response content + document.querySelectorAll('.requests-content-compact').forEach(function(container) { + applyRequestResponseHighlighting(container); + }); }); function toggleAllSteps() { diff --git a/scripts/install-pre-commit-hook b/scripts/install-pre-commit-hook index c26f6e54..2461890c 100644 --- a/scripts/install-pre-commit-hook +++ b/scripts/install-pre-commit-hook @@ -11,7 +11,7 @@ PRE_COMMIT_FILE=.git/hooks/pre-commit # install pre-commit hook and make it executable function install() { go get mvdan.cc/gofumpt - go get github.com/incu6us/goimports-reviser/v2@latest + go get github.com/incu6us/goimports-reviser/v3@latest cat > $PRE_COMMIT_FILE <<'EOF' #!/bin/bash diff --git a/step_ui.go b/step_ui.go index 9971ca08..c499321b 100644 --- a/step_ui.go +++ b/step_ui.go @@ -765,6 +765,10 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err identifier = action.Identifier break } + if action.Options != nil && action.Options.Identifier != "" { + identifier = action.Options.Identifier + break + } } } stepResult.Identifier = identifier diff --git a/uixt/driver_session.go b/uixt/driver_session.go index 733f7440..b929a060 100644 --- a/uixt/driver_session.go +++ b/uixt/driver_session.go @@ -158,25 +158,42 @@ func (s *DriverSession) DELETE(urlStr string) (rawResp DriverRawResponse, err er func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte) ( rawResp DriverRawResponse, err error) { - for count := 1; count <= s.maxRetry; count++ { + var lastError error + + for attempt := 1; attempt <= s.maxRetry; attempt++ { + // Execute the request rawResp, err = s.Request(method, urlStr, rawBody) if err == nil { - return + if attempt > 1 { + log.Info().Msgf("request succeeded after %d attempts", attempt) + } + return rawResp, nil } + + lastError = err + log.Warn().Err(err).Msgf("request failed, attempt %d/%d", attempt, s.maxRetry) + + // If this was the last attempt, break + if attempt == s.maxRetry { + log.Error().Err(lastError).Msgf("all %d retry attempts failed, giving up", s.maxRetry) + break + } + + // Wait before next attempt time.Sleep(3 * time.Second) + // Try to reset the session for the next attempt if s.resetFn != nil { - log.Warn().Msg("reset driver session") - if err2 := s.resetFn(); err2 != nil { - log.Error().Err(err2).Msgf( - "failed to reset session, try count %v", count) + log.Warn().Msgf("attempting to reset driver session before attempt %d", attempt+1) + if resetErr := s.resetFn(); resetErr != nil { + log.Error().Err(resetErr).Msgf("failed to reset session, will retry without reset") } else { - log.Info().Msgf( - "reset session success, try count %v", count) + log.Info().Msg("session reset successful") } } } - return + + return nil, lastError } func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) ( diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go index f62bb0e0..23d872dc 100644 --- a/uixt/ios_driver_wda.go +++ b/uixt/ios_driver_wda.go @@ -36,13 +36,19 @@ func NewWDADriver(device *IOSDevice) (*WDADriver, error) { Session: NewDriverSession(), } - if !device.Options.LazySetup { - // setup driver - if err := driver.Setup(); err != nil { - return nil, err - } + // setup driver + if err := driver.Setup(); err != nil { + return nil, err } + // check WDA status + wdaStatus, err := driver.Status() + if err != nil { + return nil, err + } + log.Info().Interface("status", wdaStatus). + Msg("check WDA status") + // register driver session reset handler driver.Session.RegisterResetHandler(driver.Setup) @@ -146,13 +152,6 @@ func (wd *WDADriver) Setup() error { return err } - wdaStatus, err := wd.Status() - if err != nil { - return err - } - log.Info().Interface("status", wdaStatus). - Msg("check WDA status") - // create new session if err := wd.InitSession(nil); err != nil { return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) diff --git a/uixt/ios_test.go b/uixt/ios_test.go index 155ce703..55283f3f 100644 --- a/uixt/ios_test.go +++ b/uixt/ios_test.go @@ -38,8 +38,7 @@ func TestDevice_IOS_Install(t *testing.T) { func TestDriver_WDA_LazySetup(t *testing.T) { device, err := NewIOSDevice( option.WithWDAPort(8700), - option.WithWDAMjpegPort(8800), - option.WithLazySetup(true)) + option.WithWDAMjpegPort(8800)) require.Nil(t, err) driver, err := NewWDADriver(device) require.Nil(t, err) diff --git a/uixt/mcp_server.go b/uixt/mcp_server.go index 098fdb44..2c721fff 100644 --- a/uixt/mcp_server.go +++ b/uixt/mcp_server.go @@ -193,6 +193,8 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume "tap_random_rect": tempOptions.TapRandomRect, "anti_risk": tempOptions.AntiRisk, "pre_mark_operation": tempOptions.PreMarkOperation, + "reset_history": tempOptions.ResetHistory, + "match_one": tempOptions.MatchOne, } // Add boolean options only if they are true @@ -209,6 +211,18 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume if tempOptions.Index != 0 { arguments["index"] = tempOptions.Index } + if tempOptions.Interval > 0 { + arguments["interval"] = tempOptions.Interval + } + if tempOptions.Steps > 0 { + arguments["steps"] = tempOptions.Steps + } + if tempOptions.Timeout > 0 { + arguments["timeout"] = tempOptions.Timeout + } + if tempOptions.Frequency > 0 { + arguments["frequency"] = tempOptions.Frequency + } // Only set duration if it's not already set (to avoid overriding tool-specific conversions) if tempOptions.Duration > 0 { if _, exists := arguments["duration"]; !exists { @@ -288,13 +302,19 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume if tempOptions.Selector != "" { arguments["selector"] = tempOptions.Selector } -} - -func getFloat64ValueOrDefault(value float64, defaultValue float64) float64 { - if value == 0 { - return defaultValue + if tempOptions.Identifier != "" { + arguments["identifier"] = tempOptions.Identifier + } + + // Add direction option (can be string or []float64) + if tempOptions.Direction != nil { + arguments["direction"] = tempOptions.Direction + } + + // Add custom options + if len(tempOptions.Custom) > 0 { + arguments["custom"] = tempOptions.Custom } - return value } // parseActionOptions converts MCP request arguments to ActionOptions struct diff --git a/uixt/mcp_tools_ai.go b/uixt/mcp_tools_ai.go index c932f43f..7acf31e9 100644 --- a/uixt/mcp_tools_ai.go +++ b/uixt/mcp_tools_ai.go @@ -4,10 +4,11 @@ import ( "context" "fmt" - "github.com/httprunner/httprunner/v5/uixt/option" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/uixt/option" ) // ToolStartToGoal implements the start_to_goal tool call. @@ -162,7 +163,7 @@ func (t *ToolAIQuery) Implement() server.ToolHandlerFunc { return nil, err } - // Build action options from unified request + // Build all options from request arguments opts := unifiedReq.Options() // AI query logic with options diff --git a/uixt/mcp_tools_swipe.go b/uixt/mcp_tools_swipe.go index 1dfa6dd7..d624da7f 100644 --- a/uixt/mcp_tools_swipe.go +++ b/uixt/mcp_tools_swipe.go @@ -5,11 +5,12 @@ import ( "fmt" "slices" - "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/uixt/option" "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" ) // ToolSwipe implements the generic swipe tool call. @@ -124,15 +125,13 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc { swipeDirection, validDirections) } - opts := []option.ActionOption{ - option.WithDuration(getFloat64ValueOrDefault(unifiedReq.Duration, 0.5)), - option.WithPressDuration(getFloat64ValueOrDefault(unifiedReq.PressDuration, 0.1)), + // Build all options from request arguments + opts := unifiedReq.Options() + if unifiedReq.Duration == 0 { + opts = append(opts, option.WithDuration(0.5)) } - if unifiedReq.AntiRisk { - opts = append(opts, option.WithAntiRisk(true)) - } - if unifiedReq.PreMarkOperation { - opts = append(opts, option.WithPreMarkOperation(true)) + if unifiedReq.PressDuration == 0 { + opts = append(opts, option.WithPressDuration(0.1)) } // Convert direction to coordinates and perform swipe @@ -240,17 +239,8 @@ func (t *ToolSwipeCoordinate) Implement() server.ToolHandlerFunc { params := []float64{unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY} - // Build action options from the unified request - opts := []option.ActionOption{} - if unifiedReq.Duration > 0 { - opts = append(opts, option.WithDuration(unifiedReq.Duration)) - } - if unifiedReq.PressDuration > 0 { - opts = append(opts, option.WithPressDuration(unifiedReq.PressDuration)) - } - if unifiedReq.AntiRisk { - opts = append(opts, option.WithAntiRisk(true)) - } + // Build all options from request arguments + opts := unifiedReq.Options() swipeAction := prepareSwipeAction(driverExt, params, opts...) err = swipeAction(driverExt) @@ -327,7 +317,7 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc { } // Build action options from request structure - var opts []option.ActionOption + opts := unifiedReq.Options() // Add boolean options if unifiedReq.IgnoreNotFoundError { @@ -400,24 +390,8 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc { return nil, err } - // Build action options from request structure - var opts []option.ActionOption - - // Add boolean options - if unifiedReq.IgnoreNotFoundError { - opts = append(opts, option.WithIgnoreNotFoundError(true)) - } - if unifiedReq.Regex { - opts = append(opts, option.WithRegex(true)) - } - - // Add numeric options - if unifiedReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes)) - } - if unifiedReq.Index > 0 { - opts = append(opts, option.WithIndex(unifiedReq.Index)) - } + // Build all options from request arguments + opts := unifiedReq.Options() // Swipe to tap text action logic err = driverExt.SwipeToTapTexts([]string{unifiedReq.Text}, opts...) @@ -478,24 +452,8 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc { return nil, err } - // Build action options from request structure - var opts []option.ActionOption - - // Add boolean options - if unifiedReq.IgnoreNotFoundError { - opts = append(opts, option.WithIgnoreNotFoundError(true)) - } - if unifiedReq.Regex { - opts = append(opts, option.WithRegex(true)) - } - - // Add numeric options - if unifiedReq.MaxRetryTimes > 0 { - opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes)) - } - if unifiedReq.Index > 0 { - opts = append(opts, option.WithIndex(unifiedReq.Index)) - } + // Build all options from request arguments + opts := unifiedReq.Options() // Swipe to tap texts action logic err = driverExt.SwipeToTapTexts(unifiedReq.Texts, opts...) @@ -575,12 +533,10 @@ func (t *ToolDrag) Implement() server.ToolHandlerFunc { return nil, fmt.Errorf("from_x, from_y, to_x, and to_y coordinates are required") } - opts := []option.ActionOption{} - if unifiedReq.Duration > 0 { - opts = append(opts, option.WithDuration(unifiedReq.Duration/1000.0)) - } - if unifiedReq.AntiRisk { - opts = append(opts, option.WithAntiRisk(true)) + // Build all options from request arguments + opts := unifiedReq.Options() + if unifiedReq.Duration == 0 { + opts = append(opts, option.WithDuration(0.5)) } // Drag action logic diff --git a/uixt/mcp_tools_touch.go b/uixt/mcp_tools_touch.go index 19e42261..539c7bb4 100644 --- a/uixt/mcp_tools_touch.go +++ b/uixt/mcp_tools_touch.go @@ -4,10 +4,11 @@ import ( "context" "fmt" - "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/uixt/option" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" + + "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/uixt/option" ) // ToolTapXY implements the tap_xy tool call. @@ -42,14 +43,9 @@ func (t *ToolTapXY) Implement() server.ToolHandlerFunc { return nil, err } - // Get options directly since ActionOptions is now ActionOptions + // Build all options from request arguments opts := unifiedReq.Options() - // Add configurable options based on request - if unifiedReq.PreMarkOperation { - opts = append(opts, option.WithPreMarkOperation(true)) - } - // Validate required parameters if unifiedReq.X == 0 || unifiedReq.Y == 0 { return nil, fmt.Errorf("x and y coordinates are required") @@ -123,19 +119,9 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc { return nil, err } - // Get options directly since ActionOptions is now ActionOptions + // Build all options from request arguments opts := unifiedReq.Options() - // Add configurable options based on request - if unifiedReq.PreMarkOperation { - opts = append(opts, option.WithPreMarkOperation(true)) - } - - // Add AntiRisk support - if unifiedReq.AntiRisk { - opts = append(opts, option.WithAntiRisk(true)) - } - // Validate required parameters if unifiedReq.X == 0 || unifiedReq.Y == 0 { return nil, fmt.Errorf("x and y coordinates are required") @@ -208,14 +194,9 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc { return nil, err } - // Get options directly since ActionOptions is now ActionOptions + // Build all options from request arguments opts := unifiedReq.Options() - // Add configurable options based on request - if unifiedReq.PreMarkOperation { - opts = append(opts, option.WithPreMarkOperation(true)) - } - // Validate required parameters if unifiedReq.Text == "" { return nil, fmt.Errorf("text parameter is required") @@ -277,14 +258,9 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc { return nil, err } - // Get options directly since ActionOptions is now ActionOptions + // Build all options from request arguments opts := unifiedReq.Options() - // Add configurable options based on request - if unifiedReq.PreMarkOperation { - opts = append(opts, option.WithPreMarkOperation(true)) - } - // For TapByCV, we need to check if there are UI types in the options // In the original DoAction, it requires ScreenShotWithUITypes to be set // We'll add a basic implementation that triggers CV recognition diff --git a/uixt/mcp_tools_web.go b/uixt/mcp_tools_web.go index a21a7489..a34c416b 100644 --- a/uixt/mcp_tools_web.go +++ b/uixt/mcp_tools_web.go @@ -5,11 +5,12 @@ import ( "encoding/json" "fmt" - "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/uixt/option" "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" ) // ToolWebLoginNoneUI implements the web_login_none_ui tool call. @@ -170,7 +171,7 @@ func (t *ToolHoverBySelector) Implement() server.ToolHandlerFunc { if err != nil { return nil, err } - // Get options directly since ActionOptions is now ActionOptions + // Build all options from request arguments opts := unifiedReq.Options() // Hover by selector action logic @@ -228,7 +229,7 @@ func (t *ToolTapBySelector) Implement() server.ToolHandlerFunc { if err != nil { return nil, err } - // Get options directly since ActionOptions is now ActionOptions + // Build all options from request arguments opts := unifiedReq.Options() // Tap by selector action logic @@ -286,7 +287,7 @@ func (t *ToolSecondaryClickBySelector) Implement() server.ToolHandlerFunc { if err != nil { return nil, err } - // Get options directly since ActionOptions is now ActionOptions + // Build all options from request arguments opts := unifiedReq.Options() // Secondary click by selector action logic diff --git a/uixt/option/action.go b/uixt/option/action.go index b9f45b0c..f811562e 100644 --- a/uixt/option/action.go +++ b/uixt/option/action.go @@ -7,10 +7,11 @@ import ( "reflect" "strings" - "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/uixt/types" "github.com/mark3labs/mcp-go/mcp" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/uixt/types" ) type MobileAction struct { @@ -326,6 +327,10 @@ func (o *ActionOptions) Options() []ActionOption { options = append(options, WithAntiRisk(true)) } + if o.PreMarkOperation { + options = append(options, WithPreMarkOperation(true)) + } + // custom options if o.Custom != nil { for k, v := range o.Custom { diff --git a/uixt/option/device.go b/uixt/option/device.go index 1b1257ae..c40b300c 100644 --- a/uixt/option/device.go +++ b/uixt/option/device.go @@ -152,17 +152,6 @@ func WithDeviceWDAMjpegPort(port int) DeviceOption { } } -func WithDeviceLazySetup(lazySetup bool) DeviceOption { - return func(device *DeviceOptions) { - if device.IOSDeviceOptions != nil { - device.IOSDeviceOptions.LazySetup = lazySetup - } - if device.Platform == "" { - device.Platform = "ios" - } - } -} - func WithDeviceResetHomeOnStartup(reset bool) DeviceOption { return func(device *DeviceOptions) { if device.IOSDeviceOptions != nil { diff --git a/uixt/option/ios.go b/uixt/option/ios.go index 42b9b2bd..a19c716d 100644 --- a/uixt/option/ios.go +++ b/uixt/option/ios.go @@ -6,7 +6,6 @@ type IOSDeviceOptions struct { WDAPort int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port WDAMjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` - LazySetup bool `json:"lazy_setup,omitempty" yaml:"lazy_setup,omitempty"` // lazy setup WDA // switch to iOS springboard before init WDA session ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"` @@ -33,9 +32,6 @@ func (dev *IOSDeviceOptions) Options() (deviceOptions []IOSDeviceOption) { if dev.LogOn { deviceOptions = append(deviceOptions, WithWDALogOn(true)) } - if dev.LazySetup { - deviceOptions = append(deviceOptions, WithLazySetup(true)) - } if dev.ResetHomeOnStartup { deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true)) } @@ -104,12 +100,6 @@ func WithWDALogOn(logOn bool) IOSDeviceOption { } } -func WithLazySetup(lazySetup bool) IOSDeviceOption { - return func(device *IOSDeviceOptions) { - device.LazySetup = lazySetup - } -} - func WithResetHomeOnStartup(reset bool) IOSDeviceOption { return func(device *IOSDeviceOptions) { device.ResetHomeOnStartup = reset