tools: add offline rpa window watchdog
This commit is contained in:
@@ -31,6 +31,52 @@ powershell -NoProfile -ExecutionPolicy Bypass -File E:\coding\codex\isphere-ai-b
|
||||
- 不生成屏幕截图,避免因为窗口在屏幕外得到无意义图片。
|
||||
- 继续输出 UIA dump,供 RPA 选择器验证。
|
||||
|
||||
## 抗干扰能力
|
||||
|
||||
新增守护模式:
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File E:\coding\codex\isphere-ai-bridge\scripts\open-offline-real-client-window.ps1 -WindowMode Visible -WatchSeconds 30 -PollIntervalMs 500
|
||||
```
|
||||
|
||||
也可和无感模式一起使用:
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File E:\coding\codex\isphere-ai-bridge\scripts\open-offline-real-client-window.ps1 -UserSilent -WatchSeconds 30 -PollIntervalMs 500
|
||||
```
|
||||
|
||||
当前守护逻辑:
|
||||
|
||||
- 用户手动移动主窗口:检测到位置/大小偏离后自动移回目标位置。
|
||||
- 用户手动移动提示窗:检测到位置/大小偏离后自动移回目标位置。
|
||||
- 用户关闭主窗口/杀掉客户端进程:检测到主窗口消失后自动重新启动离线客户端。
|
||||
- 每次守护输出:
|
||||
- `watch_iteration_count`
|
||||
- `repair_count`
|
||||
- `relaunch_count`
|
||||
- `recovery_actions`
|
||||
|
||||
本轮实际验证:
|
||||
|
||||
1. 模拟移动窗口到 `760,260,310x610`:
|
||||
- 守护输出 `repair_count=1`
|
||||
- `recovery_actions=["repair_window_placement"]`
|
||||
- 最终窗口恢复到 `120,120,270x570`
|
||||
|
||||
2. 模拟关闭客户端进程:
|
||||
- 被关闭 PID:`11592`
|
||||
- 守护输出 `relaunch_count=1`
|
||||
- 新启动 PID:`10504`
|
||||
- `recovery_actions=["relaunch_missing_main_window","repair_after_relaunch"]`
|
||||
- 最终 UIA 根控件仍为 `frmLogin`
|
||||
|
||||
抗干扰强度结论:
|
||||
|
||||
- 对“用户移动窗口”:强。
|
||||
- 对“用户关闭窗口/进程”:中强,可自动重启。
|
||||
- 对“用户在关键发送瞬间抢焦点/操作同一窗口”:当前还不是强,需要动作级锁定和发送前后校验。
|
||||
- 对“应用崩溃/服务端不可用”:只能自动拉起窗口;业务登录/聊天能力仍受真实服务端影响。
|
||||
|
||||
本轮验证结果:
|
||||
|
||||
- 新启动进程:`IMPlatformClient.exe`
|
||||
|
||||
@@ -9,6 +9,8 @@ param(
|
||||
[int]$Y = 120,
|
||||
[int]$Width = 270,
|
||||
[int]$Height = 570,
|
||||
[int]$WatchSeconds = 0,
|
||||
[int]$PollIntervalMs = 500,
|
||||
[ValidateSet("Visible", "Offscreen", "Minimized")]
|
||||
[string]$WindowMode = "Visible",
|
||||
[switch]$SkipExtract,
|
||||
@@ -107,6 +109,19 @@ public static class ISphereOfflineWindowApi {
|
||||
"@ -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Start-OfflineClient {
|
||||
if ($script:UserSilentMode -or $script:WindowModeValue -ne "Visible" -or $script:NoActivateMode) {
|
||||
return Start-Process -FilePath $script:ClientExePath -WorkingDirectory $script:ClientDirPath -WindowStyle Minimized -PassThru
|
||||
}
|
||||
return Start-Process -FilePath $script:ClientExePath -WorkingDirectory $script:ClientDirPath -PassThru
|
||||
}
|
||||
|
||||
function Relaunch-OfflineClient {
|
||||
Stop-ExistingOfflineCopies -RootPath $script:ExtractRootPath
|
||||
Start-Sleep -Milliseconds 500
|
||||
return Start-OfflineClient
|
||||
}
|
||||
|
||||
function Move-ClientWindows([object[]]$Windows, [int]$MainX, [int]$MainY, [int]$MainWidth, [int]$MainHeight, [bool]$MakeTopMost, [bool]$Activate, [string]$Mode) {
|
||||
$hwndTop = [IntPtr]::Zero
|
||||
$topMostHandle = [IntPtr]::new(-1)
|
||||
@@ -157,6 +172,32 @@ function Move-ClientWindows([object[]]$Windows, [int]$MainX, [int]$MainY, [int]$
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ClientWindowsForProcess([object]$Process) {
|
||||
$scanResult = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $false } -RequestId "offline-real-client-watch-scan"
|
||||
if (-not $scanResult.ok -or -not $scanResult.data.windows) {
|
||||
return @()
|
||||
}
|
||||
return @($scanResult.data.windows | Where-Object {
|
||||
$_.process_name -eq "IMPlatformClient" -and
|
||||
((-not $Process) -or $_.pid -eq $Process.Id)
|
||||
})
|
||||
}
|
||||
|
||||
function Test-WindowNear([object]$Window, [int]$ExpectedX, [int]$ExpectedY, [int]$ExpectedWidth, [int]$ExpectedHeight) {
|
||||
if (-not $Window -or -not $Window.bounds) {
|
||||
return $false
|
||||
}
|
||||
$tolerance = 3
|
||||
return ([Math]::Abs([int]$Window.bounds.x - $ExpectedX) -le $tolerance) -and
|
||||
([Math]::Abs([int]$Window.bounds.y - $ExpectedY) -le $tolerance) -and
|
||||
([Math]::Abs([int]$Window.bounds.width - $ExpectedWidth) -le $tolerance) -and
|
||||
([Math]::Abs([int]$Window.bounds.height - $ExpectedHeight) -le $tolerance)
|
||||
}
|
||||
|
||||
function Repair-ClientWindowPlacement([object[]]$Windows) {
|
||||
Move-ClientWindows -Windows $Windows -MainX $script:EffectiveX -MainY $script:EffectiveY -MainWidth $script:DesiredWidth -MainHeight $script:DesiredHeight -MakeTopMost ([bool]$script:TopMostMode) -Activate (-not [bool]$script:NoActivateMode) -Mode $script:WindowModeValue | Out-Null
|
||||
}
|
||||
|
||||
function Save-WindowScreenshot([string]$OutputPath, [int]$ScreenX, [int]$ScreenY, [int]$CaptureWidth, [int]$CaptureHeight) {
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bitmap = New-Object System.Drawing.Bitmap $CaptureWidth, $CaptureHeight
|
||||
@@ -174,6 +215,7 @@ function Save-WindowScreenshot([string]$OutputPath, [int]$ScreenX, [int]$ScreenY
|
||||
$archiveFull = Resolve-RepoPath $ArchivePath
|
||||
$extractFull = Resolve-RepoPath $ExtractDir
|
||||
$script:HelperPath = Resolve-RepoPath $HelperExe
|
||||
$script:ExtractRootPath = $extractFull
|
||||
$runOut = Join-Path $repo "runs\offline-real-client-window"
|
||||
New-Item -ItemType Directory -Force -Path $runOut | Out-Null
|
||||
|
||||
@@ -185,6 +227,16 @@ $clientDir = Join-Path $extractFull ("zyl\" + $ClientRoot)
|
||||
$clientExe = Join-Path $clientDir "IMPlatformClient.exe"
|
||||
$clientConfig = Join-Path $clientDir "IMPlatformClient.exe.config"
|
||||
$requiredDependency = Join-Path $clientDir "Utilities.Lib.Base.dll"
|
||||
$script:ClientDirPath = $clientDir
|
||||
$script:ClientExePath = $clientExe
|
||||
$script:UserSilentMode = [bool]$UserSilent
|
||||
$script:NoActivateMode = [bool]$NoActivate
|
||||
$script:WindowModeValue = $WindowMode
|
||||
$script:EffectiveX = $effectiveX
|
||||
$script:EffectiveY = $effectiveY
|
||||
$script:DesiredWidth = $Width
|
||||
$script:DesiredHeight = $Height
|
||||
$script:TopMostMode = [bool]$TopMost
|
||||
|
||||
if (-not $SkipExtract -and (-not (Test-Path -LiteralPath $clientExe) -or -not (Test-Path -LiteralPath $clientConfig) -or -not (Test-Path -LiteralPath $requiredDependency))) {
|
||||
if (-not (Test-Path -LiteralPath $archiveFull)) {
|
||||
@@ -211,12 +263,7 @@ if (-not $KeepExisting) {
|
||||
|
||||
$process = $null
|
||||
if (-not $NoLaunch) {
|
||||
if ($UserSilent -or $WindowMode -ne "Visible" -or $NoActivate) {
|
||||
$process = Start-Process -FilePath $clientExe -WorkingDirectory $clientDir -WindowStyle Minimized -PassThru
|
||||
}
|
||||
else {
|
||||
$process = Start-Process -FilePath $clientExe -WorkingDirectory $clientDir -PassThru
|
||||
}
|
||||
$process = Start-OfflineClient
|
||||
}
|
||||
|
||||
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $WaitSeconds))
|
||||
@@ -276,6 +323,65 @@ if ($latePromptWindows.Count -gt 0) {
|
||||
})
|
||||
}
|
||||
|
||||
$watchIterationCount = 0
|
||||
$repairCount = 0
|
||||
$relaunchCount = 0
|
||||
$recoveryActions = @()
|
||||
|
||||
if ($WatchSeconds -gt 0) {
|
||||
$safePollIntervalMs = [Math]::Max(100, $PollIntervalMs)
|
||||
$watchDeadline = (Get-Date).AddSeconds($WatchSeconds)
|
||||
while ((Get-Date) -lt $watchDeadline) {
|
||||
Start-Sleep -Milliseconds $safePollIntervalMs
|
||||
$watchIterationCount++
|
||||
$watchedWindows = Get-ClientWindowsForProcess -Process $process
|
||||
$watchedMain = @($watchedWindows | Where-Object { [string]$_.title -ne $InfoPromptTitle }) | Select-Object -First 1
|
||||
$watchedPrompt = @($watchedWindows | Where-Object { [string]$_.title -eq $InfoPromptTitle }) | Select-Object -First 1
|
||||
|
||||
if (-not $watchedMain) {
|
||||
if (-not $NoLaunch) {
|
||||
$process = Relaunch-OfflineClient
|
||||
$relaunchCount++
|
||||
$recoveryActions += "relaunch_missing_main_window"
|
||||
|
||||
$relaunchDeadline = (Get-Date).AddSeconds([Math]::Max(1, $WaitSeconds))
|
||||
do {
|
||||
Start-Sleep -Milliseconds $safePollIntervalMs
|
||||
$watchedWindows = Get-ClientWindowsForProcess -Process $process
|
||||
$watchedMain = @($watchedWindows | Where-Object { [string]$_.title -ne $InfoPromptTitle }) | Select-Object -First 1
|
||||
} while (-not $watchedMain -and (Get-Date) -lt $relaunchDeadline)
|
||||
|
||||
if ($watchedWindows.Count -gt 0) {
|
||||
Repair-ClientWindowPlacement -Windows $watchedWindows
|
||||
$repairCount++
|
||||
$recoveryActions += "repair_after_relaunch"
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$expectedPromptX = if ($WindowMode -eq "Offscreen") { $effectiveX } else { $effectiveX + $Width + 40 }
|
||||
$expectedPromptY = if ($WindowMode -eq "Offscreen") { $effectiveY + $Height + 40 } else { $effectiveY + 40 }
|
||||
|
||||
$needsRepair = -not (Test-WindowNear -Window $watchedMain -ExpectedX $effectiveX -ExpectedY $effectiveY -ExpectedWidth $Width -ExpectedHeight $Height)
|
||||
if ($watchedPrompt) {
|
||||
$needsRepair = $needsRepair -or -not (Test-WindowNear -Window $watchedPrompt -ExpectedX $expectedPromptX -ExpectedY $expectedPromptY -ExpectedWidth 300 -ExpectedHeight 190)
|
||||
}
|
||||
|
||||
if ($needsRepair) {
|
||||
Repair-ClientWindowPlacement -Windows $watchedWindows
|
||||
$repairCount++
|
||||
$recoveryActions += "repair_window_placement"
|
||||
}
|
||||
}
|
||||
|
||||
$scanAfterMove = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $false } -RequestId "offline-real-client-final-scan-after-watch"
|
||||
$windowsAfterMove = @($scanAfterMove.data.windows | Where-Object {
|
||||
$_.process_name -eq "IMPlatformClient" -and
|
||||
((-not $process) -or $_.pid -eq $process.Id)
|
||||
})
|
||||
}
|
||||
|
||||
$mainWindowMatches = @($windowsAfterMove | Where-Object { [string]$_.title -ne $InfoPromptTitle })
|
||||
$promptWindowMatches = @($windowsAfterMove | Where-Object { [string]$_.title -eq $InfoPromptTitle })
|
||||
$mainWindow = if ($mainWindowMatches.Count -gt 0) { $mainWindowMatches[0] } else { $null }
|
||||
@@ -306,6 +412,12 @@ if (-not $NoScreenshot -and $WindowMode -eq "Visible") {
|
||||
user_silent = [bool]$UserSilent
|
||||
window_mode = $WindowMode
|
||||
no_activate = [bool]$NoActivate
|
||||
watch_seconds = $WatchSeconds
|
||||
poll_interval_ms = $PollIntervalMs
|
||||
watch_iteration_count = $watchIterationCount
|
||||
relaunch_count = $relaunchCount
|
||||
repair_count = $repairCount
|
||||
recovery_actions = $recoveryActions
|
||||
effective_bounds = @{
|
||||
x = $effectiveX
|
||||
y = $effectiveY
|
||||
|
||||
45
scripts/test-open-offline-real-client-window-watchdog.ps1
Normal file
45
scripts/test-open-offline-real-client-window-watchdog.ps1
Normal file
@@ -0,0 +1,45 @@
|
||||
param(
|
||||
[string]$ScriptPath = "scripts/open-offline-real-client-window.ps1"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")
|
||||
$target = if ([System.IO.Path]::IsPathRooted($ScriptPath)) {
|
||||
$ScriptPath
|
||||
}
|
||||
else {
|
||||
Join-Path $repo $ScriptPath
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $target)) {
|
||||
throw "script not found: $target"
|
||||
}
|
||||
|
||||
$text = Get-Content -LiteralPath $target -Raw
|
||||
|
||||
$requiredPatterns = @(
|
||||
@{ Name = "WatchSeconds parameter"; Pattern = '\[int\]\$WatchSeconds' },
|
||||
@{ Name = "PollIntervalMs parameter"; Pattern = '\[int\]\$PollIntervalMs' },
|
||||
@{ Name = "watch loop output"; Pattern = 'watch_iteration_count' },
|
||||
@{ Name = "relaunch counter output"; Pattern = 'relaunch_count' },
|
||||
@{ Name = "move repair counter output"; Pattern = 'repair_count' },
|
||||
@{ Name = "closed window recovery"; Pattern = 'Relaunch-OfflineClient' },
|
||||
@{ Name = "moved window recovery"; Pattern = 'Repair-ClientWindowPlacement' }
|
||||
)
|
||||
|
||||
$missing = @()
|
||||
foreach ($item in $requiredPatterns) {
|
||||
if ($text -notmatch $item.Pattern) {
|
||||
$missing += $item.Name
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing.Count -gt 0) {
|
||||
throw "watchdog contract missing: $($missing -join ', ')"
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
ok = $true
|
||||
script = $target
|
||||
checked = $requiredPatterns.Count
|
||||
} | ConvertTo-Json -Compress
|
||||
Reference in New Issue
Block a user