diff --git a/mobile/README.md b/mobile/README.md index fb68848d..a3266ee9 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -54,6 +54,7 @@ powershell -ExecutionPolicy Bypass -File D:\ai-contract-platform\mobile\tools\re That smoke helper will: - call the reconnect helper and rediscover the latest wireless-debug port +- wait for the target serial to reach `adb` state `device` before launching the app - optionally reinstall `build/app/outputs/flutter-apk/app-debug.apk` when it exists - verify `8001/health` and `3003/` - wake the phone, relaunch the app, and capture: @@ -92,6 +93,7 @@ Current blocker meanings: - `wireless_adb_unavailable` - the workstation cannot discover or connect to the phone's wireless ADB endpoint - re-enable or re-pair Wireless debugging on the phone, then rerun the smoke command + - if `adb devices -l` shows the serial as `offline`, wait for the phone to finish reconnecting or toggle Wireless debugging off/on once - `device_locked` - the phone is still behind the OEM lockscreen / pattern unlock surface - `login_required` diff --git a/mobile/STATUS.md b/mobile/STATUS.md index 108740a7..59c76ddf 100644 --- a/mobile/STATUS.md +++ b/mobile/STATUS.md @@ -106,9 +106,13 @@ Created on 2026-04-18. - Android true-device validation continued on the wireless X60PRO: - connected serial: `192.168.3.93:44421` - `tools/real-device-smoke.ps1` now supports `-DeviceSerial` for known wireless-debug ports + - reconnect and smoke helpers now wait for the wireless serial to reach `adb` state `device`, so a transient `offline` session is reported as a wireless ADB blocker instead of an app-surface failure - `documents-workspace` now passes through signed-in shell -> AI tools -> legal-document workspace -> type list -> builder + - legal-document type cards, builder primary actions, and draft actions now expose stable semantic labels for accessibility and automation - `tools/real-device-smoke.ps1 -DeviceSerial 192.168.3.93:44421 -ConfigureDevice -SkipInstall -SmokeDepth documents-workspace -LoginAccount -LoginPassword ` returned `SMOKE_OK=True` - artifact bundle: `mobile/qa-artifacts/real-device-smoke/2026-05-25T21-26-28-369/` + - current wireless-debug serial rotated again to `192.168.3.93:38491`; the first rerun hit `adb` `offline`, so the reconnect/smoke helpers were hardened to wait for the serial to reach `device` before continuing + - the latest `documents-workspace` retry on `192.168.3.93:38491` now exits cleanly with `SMOKE_BLOCKER=wireless_adb_unavailable` instead of an app-surface misfire, which makes the remaining issue explicit ## Phase 0/1 mobile structure progress diff --git a/mobile/lib/features/documents/presentation/legal_document_builder_screen.dart b/mobile/lib/features/documents/presentation/legal_document_builder_screen.dart index 935f278b..f252cad4 100644 --- a/mobile/lib/features/documents/presentation/legal_document_builder_screen.dart +++ b/mobile/lib/features/documents/presentation/legal_document_builder_screen.dart @@ -532,49 +532,15 @@ class _LegalDocumentBuilderScreenState spacing: 12, runSpacing: 12, children: [ - OutlinedButton.icon( - onPressed: isValidating - ? null - : _validateDraft, - icon: isValidating - ? const SizedBox( - width: 16, - height: 16, - child: - CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Icon( - Icons.verified_outlined), - label: Text( - isValidating ? '校验中...' : '校验要素', - ), - ), - OutlinedButton.icon( - onPressed: isSaving - ? null - : () => _saveDraft(), - icon: isSaving - ? const SizedBox( - width: 16, - height: 16, - child: - CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Icon(Icons.save_outlined), - label: Text( - isSaving ? '保存中...' : '保存草稿'), - ), - Tooltip( - message: 'generate-legal-document', - child: FilledButton.icon( - onPressed: isGenerating + Semantics( + label: + 'validate-legal-document-draft', + button: true, + child: OutlinedButton.icon( + onPressed: isValidating ? null - : () => _generateDraft(), - icon: isGenerating + : _validateDraft, + icon: isValidating ? const SizedBox( width: 16, height: 16, @@ -584,9 +550,59 @@ class _LegalDocumentBuilderScreenState ), ) : const Icon( - Icons.auto_awesome), + Icons.verified_outlined), label: Text( - isGenerating ? '生成中...' : '生成文书', + isValidating ? '校验中...' : '校验要素', + ), + ), + ), + Semantics( + label: 'save-legal-document-draft', + button: true, + child: OutlinedButton.icon( + onPressed: isSaving + ? null + : () => _saveDraft(), + icon: isSaving + ? const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon( + Icons.save_outlined), + label: Text( + isSaving ? '保存中...' : '保存草稿'), + ), + ), + Tooltip( + message: 'generate-legal-document', + child: Semantics( + label: 'generate-legal-document', + button: true, + child: FilledButton.icon( + onPressed: isGenerating + ? null + : () => _generateDraft(), + icon: isGenerating + ? const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon( + Icons.auto_awesome), + label: Text( + isGenerating + ? '生成中...' + : '生成文书', + ), ), ), ), @@ -881,20 +897,32 @@ class _DraftCard extends StatelessWidget { spacing: 12, runSpacing: 12, children: [ - OutlinedButton.icon( - onPressed: onRestore, - icon: const Icon(Icons.restore_outlined), - label: const Text('恢复'), + Semantics( + label: 'restore-legal-document-draft-${draft.id}', + button: true, + child: OutlinedButton.icon( + onPressed: onRestore, + icon: const Icon(Icons.restore_outlined), + label: const Text('恢复'), + ), ), - OutlinedButton.icon( - onPressed: onDelete, - icon: const Icon(Icons.delete_outline), - label: const Text('删除'), + Semantics( + label: 'delete-legal-document-draft-${draft.id}', + button: true, + child: OutlinedButton.icon( + onPressed: onDelete, + icon: const Icon(Icons.delete_outline), + label: const Text('删除'), + ), ), - FilledButton.icon( - onPressed: onGenerate, - icon: const Icon(Icons.auto_awesome), - label: const Text('生成'), + Semantics( + label: 'generate-saved-legal-document-draft-${draft.id}', + button: true, + child: FilledButton.icon( + onPressed: onGenerate, + icon: const Icon(Icons.auto_awesome), + label: const Text('生成'), + ), ), ], ), diff --git a/mobile/lib/features/documents/presentation/legal_document_type_list_screen.dart b/mobile/lib/features/documents/presentation/legal_document_type_list_screen.dart index 2e2da2fd..bdaadcae 100644 --- a/mobile/lib/features/documents/presentation/legal_document_type_list_screen.dart +++ b/mobile/lib/features/documents/presentation/legal_document_type_list_screen.dart @@ -442,10 +442,14 @@ class _DocumentTypeCard extends StatelessWidget { color: legalDocumentRiskColor(item.riskLevel), ), const SizedBox(height: 10), - FilledButton.icon( - onPressed: onOpen, - icon: const Icon(Icons.edit_note_outlined), - label: const Text('开始起草'), + Semantics( + label: 'open-legal-document-${item.code}', + button: true, + child: FilledButton.icon( + onPressed: onOpen, + icon: const Icon(Icons.edit_note_outlined), + label: const Text('开始起草'), + ), ), ], ), diff --git a/mobile/tools/real-device-smoke.ps1 b/mobile/tools/real-device-smoke.ps1 index 7f42e4cd..905e448a 100644 --- a/mobile/tools/real-device-smoke.ps1 +++ b/mobile/tools/real-device-smoke.ps1 @@ -80,6 +80,34 @@ function Get-ConnectedSerial { return $serialLine } +function Wait-ForAdbDeviceOnline { + param( + [string]$Serial, + [int]$RetryCount = 10, + [int]$DelaySeconds = 2, + [switch]$RepairOffline + ) + + $repairAttempted = $false + for ($attempt = 1; $attempt -le $RetryCount; $attempt++) { + $state = ((& $script:adb -s $Serial get-state 2>&1) -join [Environment]::NewLine).Trim() + if ($state -eq "device") { + return $true + } + + if ($RepairOffline -and -not $repairAttempted -and $state -match 'offline') { + & $script:adb reconnect offline | Out-Null + $repairAttempted = $true + } + + if ($attempt -lt $RetryCount) { + Start-Sleep -Seconds $DelaySeconds + } + } + + return $false +} + function Get-AppSurface { param([string]$UiDump) @@ -260,6 +288,10 @@ function Get-SmokeState { [string]$PackageName ) + if (-not (Wait-ForAdbDeviceOnline -Serial $Serial -RetryCount 3 -DelaySeconds 1)) { + throw "Wireless ADB device '$Serial' is not online." + } + $topText = (& $script:adb -s $Serial shell dumpsys activity top) -join [Environment]::NewLine $topPath = Join-Path $ArtifactDir "$Label-activity-top.txt" Save-TextArtifact -Path $topPath -Text $topText @@ -613,13 +645,8 @@ try { $serial = $DeviceSerial.Trim() $connectOutput = & $adb connect $serial 2>&1 $connectOutput | ForEach-Object { Write-Host $_ } - $connectedDevices = & $adb devices 2>$null - $isConnected = $connectedDevices | - ForEach-Object { $_.ToString().Trim() } | - Where-Object { $_ -match "^$([regex]::Escape($serial))\s+device$" } | - Select-Object -First 1 - if ($LASTEXITCODE -ne 0 -or -not $isConnected) { - throw "Failed to connect to specified device serial '$serial'." + if (-not (Wait-ForAdbDeviceOnline -Serial $serial -RetryCount 8 -DelaySeconds 2 -RepairOffline)) { + throw "Failed to connect specified device serial '$serial' into adb 'device' state." } if ($ConfigureDevice) { & $adb -s $serial shell settings put system screen_off_timeout 1800000 | Out-Null @@ -627,6 +654,9 @@ try { } } else { $serial = Get-ConnectedSerial -ReconnectScript $reconnectScript -ExpectedIpPrefix $ExpectedIp -ShouldConfigureDevice:$ConfigureDevice.IsPresent + if (-not (Wait-ForAdbDeviceOnline -Serial $serial -RetryCount 8 -DelaySeconds 2 -RepairOffline)) { + throw "Failed to connect discovered device serial '$serial' into adb 'device' state." + } } } catch { $connectionError = ($_.Exception.Message -replace '\s+', ' ').Trim() diff --git a/mobile/tools/reconnect-wireless-device.ps1 b/mobile/tools/reconnect-wireless-device.ps1 index 8aa0236d..86d59d20 100644 --- a/mobile/tools/reconnect-wireless-device.ps1 +++ b/mobile/tools/reconnect-wireless-device.ps1 @@ -11,6 +11,34 @@ if (-not (Test-Path $adb)) { throw "adb.exe not found at $adb" } +function Wait-ForDeviceState { + param( + [string]$Serial, + [int]$Attempts, + [int]$DelaySeconds, + [switch]$RepairOffline + ) + + $repairAttempted = $false + for ($attempt = 1; $attempt -le $Attempts; $attempt++) { + $state = ((& $adb -s $Serial get-state 2>&1) -join [Environment]::NewLine).Trim() + if ($state -eq "device") { + return $true + } + + if ($RepairOffline -and -not $repairAttempted -and $state -match 'offline') { + & $adb reconnect offline | Out-Null + $repairAttempted = $true + } + + if ($attempt -lt $Attempts) { + Start-Sleep -Seconds $DelaySeconds + } + } + + return $false +} + $pattern = '^(?\S+)\s+_adb-tls-connect\._tcp\s+(?\d+\.\d+\.\d+\.\d+):(?\d+)$' $candidates = @() @@ -48,15 +76,19 @@ $connectedSerial = $null foreach ($candidate in $candidates) { $serial = "$($candidate.Host):$($candidate.Port)" $connectOutput = & $adb connect $serial 2>&1 - if ($LASTEXITCODE -eq 0) { + $connectOutput | ForEach-Object { Write-Host $_ } + + if (Wait-ForDeviceState -Serial $serial -Attempts $RetryCount -DelaySeconds $DelaySeconds -RepairOffline) { $connectedSerial = $serial break } + + & $adb disconnect $serial | Out-Null } if (-not $connectedSerial) { $candidateList = ($candidates | ForEach-Object { "$($_.Host):$($_.Port)" }) -join ", " - throw "Failed to connect to any discovered wireless device. Candidates: $candidateList" + throw "Failed to connect any discovered wireless device into adb 'device' state. Candidates: $candidateList" } if ($ConfigureDevice) {