372 lines
14 KiB
PowerShell
372 lines
14 KiB
PowerShell
param(
|
|
[string]$HelperExe = "runs/win-helper/ISphereWinHelper.exe",
|
|
[int]$WaitSeconds = 10,
|
|
[int]$KeepOpenSeconds = 0,
|
|
[int]$X = 160,
|
|
[int]$Y = 120,
|
|
[int]$Width = 760,
|
|
[int]$Height = 560,
|
|
[ValidateSet("Visible", "Offscreen", "Minimized")]
|
|
[string]$WindowMode = "Visible",
|
|
[switch]$ProbeClick,
|
|
[switch]$NoScreenshot,
|
|
[switch]$NoActivate,
|
|
[switch]$TopMost
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$repo = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")
|
|
$helperPath = Join-Path $repo $HelperExe
|
|
$runOut = Join-Path $repo "runs\offline-chat-window"
|
|
New-Item -ItemType Directory -Force -Path $runOut | Out-Null
|
|
|
|
function Resolve-RepoPath([string]$PathValue) {
|
|
if ([System.IO.Path]::IsPathRooted($PathValue)) {
|
|
return $PathValue
|
|
}
|
|
return (Join-Path $repo $PathValue)
|
|
}
|
|
|
|
function Invoke-HelperJson([string]$Op, [hashtable]$OpArgs, [string]$RequestId, [int]$TimeoutMs = 5000) {
|
|
$request = @{
|
|
protocol = "isphere.helper.v1"
|
|
request_id = $RequestId
|
|
op = $Op
|
|
timeout_ms = $TimeoutMs
|
|
args = $OpArgs
|
|
}
|
|
$json = $request | ConvertTo-Json -Depth 16 -Compress
|
|
$output = $json | & $script:HelperPath --json
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "helper exited with code $LASTEXITCODE for op $Op. Output: $output"
|
|
}
|
|
try {
|
|
return $output | ConvertFrom-Json
|
|
}
|
|
catch {
|
|
throw "helper output was not JSON for op $Op`: $output"
|
|
}
|
|
}
|
|
|
|
function Add-NativeWindowApi {
|
|
Add-Type @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
public static class ISphereOfflineChatProbeWindowApi {
|
|
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
|
}
|
|
"@ -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Set-ProbeWindowPlacement([string]$HwndText) {
|
|
$hwndValue = [Convert]::ToInt64(($HwndText -replace "^0x", ""), 16)
|
|
$hwnd = [IntPtr]::new($hwndValue)
|
|
$insertAfter = if ($TopMost) { [IntPtr]::new(-1) } else { [IntPtr]::Zero }
|
|
$flags = 0x0040
|
|
if ($NoActivate) {
|
|
$flags = $flags -bor 0x0010
|
|
}
|
|
|
|
if ($WindowMode -eq "Minimized") {
|
|
[ISphereOfflineChatProbeWindowApi]::ShowWindow($hwnd, 6) | Out-Null
|
|
return
|
|
}
|
|
|
|
$effectiveX = $X
|
|
$effectiveY = $Y
|
|
if ($WindowMode -eq "Offscreen") {
|
|
$effectiveX = -32000
|
|
$effectiveY = -32000
|
|
}
|
|
|
|
[ISphereOfflineChatProbeWindowApi]::ShowWindow($hwnd, 9) | Out-Null
|
|
[ISphereOfflineChatProbeWindowApi]::SetWindowPos($hwnd, $insertAfter, $effectiveX, $effectiveY, $Width, $Height, [uint32]$flags) | Out-Null
|
|
if (-not $NoActivate) {
|
|
[ISphereOfflineChatProbeWindowApi]::SetForegroundWindow($hwnd) | 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
|
|
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
try {
|
|
$graphics.CopyFromScreen(($ScreenX - 20), ($ScreenY - 20), 0, 0, $bitmap.Size)
|
|
$bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
}
|
|
finally {
|
|
$graphics.Dispose()
|
|
$bitmap.Dispose()
|
|
}
|
|
}
|
|
|
|
function New-Sha256Hex([string]$Value) {
|
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
|
|
return -join ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") })
|
|
}
|
|
finally {
|
|
$sha.Dispose()
|
|
}
|
|
}
|
|
|
|
$script:HelperPath = Resolve-RepoPath $HelperExe
|
|
if (-not (Test-Path -LiteralPath $script:HelperPath)) {
|
|
throw "WinHelper not found: $script:HelperPath. Run scripts\build-win-helper.ps1 first."
|
|
}
|
|
|
|
$timestamp = (Get-Date).ToString("yyyyMMdd-HHmmss")
|
|
$windowTitle = "iSphere Offline Chat Window Probe " + ([guid]::NewGuid().ToString("N").Substring(0, 8))
|
|
$launcherScript = Join-Path $runOut ("offline-chat-window-probe-$timestamp.ps1")
|
|
$markerPath = Join-Path $runOut ("offline-chat-window-probe-marker-$timestamp.json")
|
|
$statePath = Join-Path $runOut ("offline-chat-window-probe-state-$timestamp.json")
|
|
$dumpFile = Join-Path $runOut ("uia-dump-offline-chat-window-probe-$timestamp.json")
|
|
$screenshotFile = Join-Path $runOut ("offline-chat-window-probe-$timestamp.png")
|
|
|
|
$escapedTitle = $windowTitle.Replace("'", "''")
|
|
$escapedMarker = $markerPath.Replace("'", "''")
|
|
$escapedState = $statePath.Replace("'", "''")
|
|
|
|
$formScript = @"
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type -AssemblyName System.Drawing
|
|
`$form = New-Object System.Windows.Forms.Form
|
|
`$form.Name = 'frmP2PChat'
|
|
`$form.Text = '$escapedTitle'
|
|
`$form.StartPosition = 'Manual'
|
|
`$form.Location = New-Object System.Drawing.Point(160, 120)
|
|
`$form.Size = New-Object System.Drawing.Size(760, 560)
|
|
`$form.KeyPreview = `$true
|
|
|
|
`$layout = New-Object System.Windows.Forms.TableLayoutPanel
|
|
`$layout.Name = 'chatRootLayout'
|
|
`$layout.Dock = 'Fill'
|
|
`$layout.RowCount = 5
|
|
`$layout.ColumnCount = 1
|
|
`$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 38))) | Out-Null
|
|
`$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 55))) | Out-Null
|
|
`$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 42))) | Out-Null
|
|
`$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 45))) | Out-Null
|
|
`$layout.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 48))) | Out-Null
|
|
`$form.Controls.Add(`$layout)
|
|
|
|
`$search = New-Object System.Windows.Forms.TextBox
|
|
`$search.Name = 'skinAlphaTxt'
|
|
`$search.Text = 'search contact placeholder'
|
|
`$search.Dock = 'Fill'
|
|
`$layout.Controls.Add(`$search, 0, 0)
|
|
|
|
`$recv = New-Object System.Windows.Forms.RichTextBox
|
|
`$recv.Name = 'rtbRecvMessage'
|
|
`$recv.ReadOnly = `$true
|
|
`$recv.Text = 'Offline chat window probe. This mimics frmP2PChat for UIA identify, locate, and click verification only.'
|
|
`$recv.Dock = 'Fill'
|
|
`$layout.Controls.Add(`$recv, 0, 1)
|
|
|
|
`$toolbar = New-Object System.Windows.Forms.FlowLayoutPanel
|
|
`$toolbar.Name = 'chatToolbar'
|
|
`$toolbar.Dock = 'Fill'
|
|
`$toolbar.FlowDirection = 'LeftToRight'
|
|
`$layout.Controls.Add(`$toolbar, 0, 2)
|
|
|
|
`$file = New-Object System.Windows.Forms.Button
|
|
`$file.Name = 'btnSendFile'
|
|
`$file.Text = 'Send File'
|
|
`$file.Width = 92
|
|
`$file.Height = 30
|
|
`$file.Add_Click({
|
|
`$payload = [ordered]@{
|
|
clicked = `$true
|
|
action = 'file_button_probe_only'
|
|
sent_real_message = `$false
|
|
uploaded_real_file = `$false
|
|
at = (Get-Date).ToString('o')
|
|
} | ConvertTo-Json -Compress
|
|
Set-Content -LiteralPath '$escapedMarker' -Value `$payload -Encoding UTF8
|
|
})
|
|
`$toolbar.Controls.Add(`$file)
|
|
|
|
`$offline = New-Object System.Windows.Forms.Label
|
|
`$offline.Name = 'offlineSendBlocker'
|
|
`$offline.Text = 'Offline probe only: no server connection and no real message is sent.'
|
|
`$offline.AutoSize = `$true
|
|
`$offline.Padding = New-Object System.Windows.Forms.Padding(8)
|
|
`$toolbar.Controls.Add(`$offline)
|
|
|
|
`$send = New-Object System.Windows.Forms.RichTextBox
|
|
`$send.Name = 'rtbSendMessage'
|
|
`$send.Text = ''
|
|
`$send.Dock = 'Fill'
|
|
`$layout.Controls.Add(`$send, 0, 3)
|
|
|
|
`$buttonPanel = New-Object System.Windows.Forms.FlowLayoutPanel
|
|
`$buttonPanel.Name = 'sendButtonPanel'
|
|
`$buttonPanel.Dock = 'Fill'
|
|
`$buttonPanel.FlowDirection = 'RightToLeft'
|
|
`$layout.Controls.Add(`$buttonPanel, 0, 4)
|
|
|
|
`$sendButton = New-Object System.Windows.Forms.Button
|
|
`$sendButton.Name = 'btnSend'
|
|
`$sendButton.Text = 'Send(S)'
|
|
`$sendButton.Width = 92
|
|
`$sendButton.Height = 32
|
|
`$sendButton.Add_Click({
|
|
`$payload = [ordered]@{
|
|
clicked = `$true
|
|
action = 'send_button_probe_only'
|
|
text = `$send.Text
|
|
text_length = `$send.Text.Length
|
|
sent_real_message = `$false
|
|
uploaded_real_file = `$false
|
|
at = (Get-Date).ToString('o')
|
|
} | ConvertTo-Json -Compress
|
|
Set-Content -LiteralPath '$escapedMarker' -Value `$payload -Encoding UTF8
|
|
})
|
|
`$buttonPanel.Controls.Add(`$sendButton)
|
|
|
|
`$form.Add_Shown({
|
|
`$state = [ordered]@{
|
|
ok = `$true
|
|
window_kind = 'offline_chat_window_probe'
|
|
automation_id = `$form.Name
|
|
title = `$form.Text
|
|
process_id = [System.Diagnostics.Process]::GetCurrentProcess().Id
|
|
marker_path = '$escapedMarker'
|
|
at = (Get-Date).ToString('o')
|
|
} | ConvertTo-Json -Compress
|
|
Set-Content -LiteralPath '$escapedState' -Value `$state -Encoding UTF8
|
|
})
|
|
|
|
[void]`$form.ShowDialog()
|
|
"@
|
|
|
|
Set-Content -LiteralPath $launcherScript -Value $formScript -Encoding UTF8
|
|
|
|
$process = Start-Process powershell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $launcherScript) -PassThru
|
|
$candidate = $null
|
|
$scan = $null
|
|
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $WaitSeconds))
|
|
do {
|
|
Start-Sleep -Milliseconds 300
|
|
$scan = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $true } -RequestId "offline-chat-probe-scan"
|
|
if ($scan.ok -and $scan.data.windows) {
|
|
$candidate = $scan.data.windows | Where-Object {
|
|
$_.pid -eq $process.Id -and $_.title -eq $windowTitle
|
|
} | Select-Object -First 1
|
|
}
|
|
} while (-not $candidate -and (Get-Date) -lt $deadline)
|
|
|
|
if (-not $candidate) {
|
|
if ($process -and -not $process.HasExited) {
|
|
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
|
}
|
|
throw "offline chat probe window not found. Last scan: $($scan | ConvertTo-Json -Depth 12 -Compress)"
|
|
}
|
|
|
|
Add-NativeWindowApi
|
|
Set-ProbeWindowPlacement -HwndText $candidate.hwnd
|
|
Start-Sleep -Milliseconds 500
|
|
|
|
$dump = Invoke-HelperJson -Op "dump_uia" -OpArgs @{
|
|
hwnd = $candidate.hwnd
|
|
max_depth = 8
|
|
max_children = 200
|
|
include_text = $true
|
|
} -RequestId "offline-chat-probe-dump-uia"
|
|
$dump | ConvertTo-Json -Depth 40 | Set-Content -LiteralPath $dumpFile -Encoding UTF8
|
|
|
|
$classify = Invoke-HelperJson -Op "probe_send_uia_controls" -OpArgs @{
|
|
hwnd = $candidate.hwnd
|
|
max_depth = 8
|
|
max_children = 200
|
|
} -RequestId "offline-chat-probe-classify-uia"
|
|
|
|
$probeClickedUi = $false
|
|
$probeTypedText = $false
|
|
$probeMarkerFound = $false
|
|
$probeMarkerTextMatches = $false
|
|
$probeMarker = $null
|
|
$probeContent = ""
|
|
$probeAction = $null
|
|
|
|
if ($ProbeClick) {
|
|
$probeContent = "Codex offline chat probe " + ([guid]::NewGuid().ToString("N").Substring(0, 8))
|
|
$probeHash = New-Sha256Hex $probeContent
|
|
$probeAction = Invoke-HelperJson -Op "uia_send_message" -OpArgs @{
|
|
hwnd = $candidate.hwnd
|
|
send_editor_automation_id = "rtbSendMessage"
|
|
send_button_automation_id = "btnSend"
|
|
target_ref = "offline-chat-window-probe"
|
|
content_text = $probeContent
|
|
content_sha256 = $probeHash
|
|
} -RequestId "offline-chat-probe-click"
|
|
$probeClickedUi = [bool]($probeAction.ok -and $probeAction.data.clicked_ui)
|
|
$probeTypedText = [bool]($probeAction.ok -and $probeAction.data.typed_text)
|
|
|
|
for ($i = 0; $i -lt 30 -and -not (Test-Path -LiteralPath $markerPath); $i++) {
|
|
Start-Sleep -Milliseconds 100
|
|
}
|
|
$probeMarkerFound = Test-Path -LiteralPath $markerPath
|
|
if ($probeMarkerFound) {
|
|
$probeMarker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json
|
|
$probeMarkerTextMatches = [string]$probeMarker.text -eq $probeContent
|
|
}
|
|
}
|
|
|
|
$screenshotPathForOutput = $null
|
|
if (-not $NoScreenshot -and $WindowMode -eq "Visible") {
|
|
Save-WindowScreenshot -OutputPath $screenshotFile -ScreenX $X -ScreenY $Y -CaptureWidth ($Width + 40) -CaptureHeight ($Height + 40)
|
|
$screenshotPathForOutput = $screenshotFile
|
|
}
|
|
|
|
if ($KeepOpenSeconds -gt 0) {
|
|
Start-Sleep -Seconds $KeepOpenSeconds
|
|
if ($process -and -not $process.HasExited) {
|
|
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
$rootAutomationId = ""
|
|
if ($dump -and $dump.ok -and $dump.data -and $dump.data.root) {
|
|
$rootAutomationId = [string]$dump.data.root.automation_id
|
|
}
|
|
|
|
$flags = $null
|
|
if ($classify -and $classify.ok -and $classify.data -and $classify.data.flags) {
|
|
$flags = $classify.data.flags
|
|
}
|
|
|
|
[ordered]@{
|
|
ok = [bool]($dump.ok -and $classify.ok)
|
|
window_kind = "offline_chat_window_probe"
|
|
process_id = $process.Id
|
|
hwnd = $candidate.hwnd
|
|
title = $windowTitle
|
|
root_automation_id = $rootAutomationId
|
|
root_control_type = if ($dump.ok) { [string]$dump.data.root.control_type } else { "" }
|
|
send_editor_found = [bool]($flags -and $flags.has_send_editor)
|
|
send_button_found = [bool]($flags -and $flags.has_send_button)
|
|
file_button_found = [bool]($flags -and $flags.has_file_menu)
|
|
receive_document_found = [bool]($flags -and $flags.has_receive_document)
|
|
offline_blocker_visible = [bool]($flags -and $flags.offline_blocker_visible)
|
|
route_hint = if ($flags) { [string]$flags.route_hint } else { "" }
|
|
probe_click_requested = [bool]$ProbeClick
|
|
probe_clicked_ui = $probeClickedUi
|
|
probe_typed_text = $probeTypedText
|
|
probe_click_marker_found = $probeMarkerFound
|
|
probe_marker_text_matches = $probeMarkerTextMatches
|
|
probe_action_ok = if ($probeAction) { [bool]$probeAction.ok } else { $false }
|
|
probe_action_mode = if ($probeAction -and $probeAction.ok) { [string]$probeAction.data.action_mode } else { "" }
|
|
sent_real_message = $false
|
|
uploaded_real_file = $false
|
|
marker_path = $markerPath
|
|
state_path = $statePath
|
|
launcher_script = $launcherScript
|
|
uia_dump_file = $dumpFile
|
|
screenshot_file = $screenshotPathForOutput
|
|
alive_after_return = if ($KeepOpenSeconds -gt 0) { $false } else { $null -ne (Get-Process -Id $process.Id -ErrorAction SilentlyContinue) }
|
|
} | ConvertTo-Json -Depth 16
|