Files
isphere-ai-bridge/scripts/open-offline-chat-window-probe.ps1
2026-07-11 20:21:41 +08:00

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