tools: add offline chat window probe
This commit is contained in:
@@ -0,0 +1,120 @@
|
|||||||
|
# A-route open_conversation 窗口验证说明
|
||||||
|
|
||||||
|
日期:2026-07-11
|
||||||
|
分支:`codex/a-route-rpa-send`
|
||||||
|
|
||||||
|
## 本轮目标
|
||||||
|
|
||||||
|
用户明确当前先不要求真实发送消息,只要求“打开那个会话窗口”,用来验证数字员工是否能:
|
||||||
|
|
||||||
|
1. 识别会话窗口。
|
||||||
|
2. 定位发送编辑框。
|
||||||
|
3. 定位发送按钮/文件按钮。
|
||||||
|
4. 执行一次可审计的点击。
|
||||||
|
|
||||||
|
本地环境没有 iSphere 服务端,无法登录,所以真实会话窗口不能通过正常业务路径打开。
|
||||||
|
|
||||||
|
## 已实现:离线会话窗口探针
|
||||||
|
|
||||||
|
新增脚本:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File E:\coding\codex\isphere-ai-bridge\scripts\open-offline-chat-window-probe.ps1 -ProbeClick
|
||||||
|
```
|
||||||
|
|
||||||
|
它会打开一个本地 WinForms 会话窗口探针,不连接服务端,不发送真实消息。关键 UIA 标识对齐真实会话窗口/既有 RPA 选择器:
|
||||||
|
|
||||||
|
| 目标 | AutomationId / Name |
|
||||||
|
| --- | --- |
|
||||||
|
| 会话窗口 | `frmP2PChat` |
|
||||||
|
| 联系人搜索框 | `skinAlphaTxt` |
|
||||||
|
| 消息展示区 | `rtbRecvMessage` |
|
||||||
|
| 发送编辑框 | `rtbSendMessage` |
|
||||||
|
| 发送按钮 | `btnSend` |
|
||||||
|
| 文件按钮 | `btnSendFile` |
|
||||||
|
| 离线提示 | `offlineSendBlocker` |
|
||||||
|
|
||||||
|
当传入 `-ProbeClick` 时,脚本会调用当前分支已有的 WinHelper UIA 动作,把文本写入 `rtbSendMessage` 并点击 `btnSend`。按钮点击只写本地 marker 文件,字段固定为:
|
||||||
|
|
||||||
|
- `sent_real_message=false`
|
||||||
|
- `uploaded_real_file=false`
|
||||||
|
|
||||||
|
因此这个动作只验证 UIA 识别、定位、写入、点击链路,不触发真实消息发送。
|
||||||
|
|
||||||
|
## 自动化测试
|
||||||
|
|
||||||
|
新增测试:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File E:\coding\codex\isphere-ai-bridge\scripts\test-open-offline-chat-window-probe.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
测试断言:
|
||||||
|
|
||||||
|
- 根窗口 `root_automation_id=frmP2PChat`
|
||||||
|
- 找到 `rtbSendMessage`
|
||||||
|
- 找到 `btnSend`
|
||||||
|
- 找到 `btnSendFile`
|
||||||
|
- `ProbeClick` 后写入 marker
|
||||||
|
- marker 文本与 UIA 写入文本一致
|
||||||
|
- `sent_real_message=false`
|
||||||
|
- `uploaded_real_file=false`
|
||||||
|
|
||||||
|
## 真实 `frmP2PChat` 破解进度
|
||||||
|
|
||||||
|
已继续尝试直接构造真实类型:
|
||||||
|
|
||||||
|
```text
|
||||||
|
IMPP.Client.Business.ChatManager.SingleChat.frmP2PChat
|
||||||
|
```
|
||||||
|
|
||||||
|
构造函数签名:
|
||||||
|
|
||||||
|
```text
|
||||||
|
frmP2PChat(com.vision.smack.Chat chat, string pluginInfo, string extendTabJson)
|
||||||
|
```
|
||||||
|
|
||||||
|
已补过的离线依赖包括:
|
||||||
|
|
||||||
|
- `IMPPManager.Instance.MessageCenter`
|
||||||
|
- `IMPPManager.Instance.LogonUser`
|
||||||
|
- `IMPPManager.Instance.UserInfo`
|
||||||
|
- `SmarkManager.Connection`
|
||||||
|
- `SmarkManager.RosterManager`
|
||||||
|
- `SmarkManager.PresenceManager`
|
||||||
|
- `SmarkManager.P2PChatManager`
|
||||||
|
- `ConfigSystemManager.config`
|
||||||
|
- `P2PChat.OtherJid`
|
||||||
|
|
||||||
|
仍未成功打开真实窗体。当前阻塞不再是脚本能力,而是构造函数内部把窗体初始化、登录态、连接态、名册状态、消息中心事件、配置状态混在一起。IL 证据显示构造函数中直接访问:
|
||||||
|
|
||||||
|
- `IMPPManager.Instance.Connection.add_OnConnectError`
|
||||||
|
- `IMPPManager.Instance.Connection.add_OnReConnectOk`
|
||||||
|
- `P2PChat.OtherJid.getBareJid`
|
||||||
|
- `IMPPManager.Instance.UserInfo.get_Jid`
|
||||||
|
- `UCChatSendMessageBox.GetChatRichTextBox`
|
||||||
|
- `UCChatSendMessageBox.GetReceiptCheckBox`
|
||||||
|
- `IMPPManager.Instance.GetMessageCenter().add_OnTcpMessageArrived`
|
||||||
|
- `IMPPManager.Instance.RosterManager.UpdateStrangerStatus`
|
||||||
|
- `Config.BaseConfig.VisualPhoneEnable`
|
||||||
|
|
||||||
|
这说明真实 `frmP2PChat` 不是独立窗口类;它要求完整登录运行态。没有服务端/登录态时继续硬构造,投入会越来越像“重建一个假客户端运行时”。
|
||||||
|
|
||||||
|
## 当前业务结论
|
||||||
|
|
||||||
|
从“验证 RPA 能否识别、定位、点击会话窗口”的业务目标看,本轮已经可验证:
|
||||||
|
|
||||||
|
- 会话窗口可打开。
|
||||||
|
- 核心 AutomationId 可识别。
|
||||||
|
- 文本可写入。
|
||||||
|
- 按钮可点击。
|
||||||
|
- 点击结果可留本地审计 marker。
|
||||||
|
- 全程不发真实消息、不上传真实文件。
|
||||||
|
|
||||||
|
从“真实 iSphere 会话窗口”的目标看,仍需要在线登录环境或继续补完整客户端运行态。
|
||||||
|
|
||||||
|
## 下一步建议
|
||||||
|
|
||||||
|
1. 当前分支先使用 `open-offline-chat-window-probe.ps1` 验证 RPA 识别/定位/点击效果。
|
||||||
|
2. 在线环境拿到真实登录后的 `frmP2PChat` HWND 后,用同一套 WinHelper 选择器直接验证真实窗口。
|
||||||
|
3. 如果必须离线打开真实窗体,再继续走“补完整 IMPPManager/SmarkManager/Config/BaseConfig/MessageCenter 运行态”的破解路线。
|
||||||
@@ -93,3 +93,23 @@
|
|||||||
- [x] Run `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-win-helper.ps1`.
|
- [x] Run `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-win-helper.ps1`.
|
||||||
- [x] Run `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-go-mcp.ps1`.
|
- [x] Run `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-go-mcp.ps1`.
|
||||||
- [x] Commit and push branch `codex/a-route-rpa-send`.
|
- [x] Commit and push branch `codex/a-route-rpa-send`.
|
||||||
|
|
||||||
|
## Task 6: Open conversation window probe for A-route RPA
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/open-offline-chat-window-probe.ps1`
|
||||||
|
- Create: `scripts/test-open-offline-chat-window-probe.ps1`
|
||||||
|
- Create: `docs/source-discovery/2026-07-11-a-route-open-conversation-window.md`
|
||||||
|
|
||||||
|
**Purpose:** local environment cannot log in, but RPA still needs a stable
|
||||||
|
conversation-window surface to prove identify/locate/click behavior. This task
|
||||||
|
adds a no-network WinForms probe using the real selector names
|
||||||
|
`frmP2PChat`, `rtbRecvMessage`, `rtbSendMessage`, `btnSend`, and
|
||||||
|
`btnSendFile`.
|
||||||
|
|
||||||
|
- [x] Write failing test proving the open-conversation probe script is required.
|
||||||
|
- [x] Implement the offline chat-window probe.
|
||||||
|
- [x] Verify UIA classification finds send editor, send button, and file button.
|
||||||
|
- [x] Verify `-ProbeClick` writes text and clicks `btnSend`.
|
||||||
|
- [x] Verify marker says `sent_real_message=false` and `uploaded_real_file=false`.
|
||||||
|
- [x] Document why direct offline construction of the real `frmP2PChat` is still blocked by login/runtime singletons.
|
||||||
|
|||||||
371
scripts/open-offline-chat-window-probe.ps1
Normal file
371
scripts/open-offline-chat-window-probe.ps1
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
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
|
||||||
62
scripts/test-open-offline-chat-window-probe.ps1
Normal file
62
scripts/test-open-offline-chat-window-probe.ps1
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
param(
|
||||||
|
[string]$HelperExe = "runs/win-helper/ISphereWinHelper.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$repo = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")
|
||||||
|
$scriptPath = Join-Path $repo "scripts\open-offline-chat-window-probe.ps1"
|
||||||
|
$helperPath = Join-Path $repo $HelperExe
|
||||||
|
|
||||||
|
function Assert-True([bool]$Condition, [string]$Message) {
|
||||||
|
if (-not $Condition) {
|
||||||
|
throw $Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $helperPath)) {
|
||||||
|
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repo "scripts\build-win-helper.ps1") | Out-Host
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "build-win-helper.ps1 failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-True (Test-Path -LiteralPath $scriptPath) "missing script: $scriptPath"
|
||||||
|
|
||||||
|
$output = & powershell -NoProfile -ExecutionPolicy Bypass -File $scriptPath `
|
||||||
|
-HelperExe $HelperExe `
|
||||||
|
-KeepOpenSeconds 2 `
|
||||||
|
-ProbeClick `
|
||||||
|
-NoScreenshot
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "open-offline-chat-window-probe.ps1 exited with code $LASTEXITCODE. Output: $output"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $output | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "script output was not JSON: $output"
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-True ([bool]$result.ok) "result.ok must be true"
|
||||||
|
Assert-True ([string]$result.window_kind -eq "offline_chat_window_probe") "window_kind mismatch"
|
||||||
|
Assert-True ([string]$result.root_automation_id -eq "frmP2PChat") "root automation id must be frmP2PChat"
|
||||||
|
Assert-True ([bool]$result.send_editor_found) "rtbSendMessage must be found"
|
||||||
|
Assert-True ([bool]$result.send_button_found) "btnSend must be found"
|
||||||
|
Assert-True ([bool]$result.file_button_found) "btnSendFile or btnFile must be found"
|
||||||
|
Assert-True ([bool]$result.probe_click_marker_found) "probe click marker must be written"
|
||||||
|
Assert-True ([bool]$result.probe_clicked_ui) "probe must click the UI button"
|
||||||
|
Assert-True ([bool]$result.probe_typed_text) "probe must type into the send editor"
|
||||||
|
Assert-True ([bool]$result.probe_marker_text_matches) "probe marker text must match typed text"
|
||||||
|
Assert-True (-not [bool]$result.sent_real_message) "probe must not send a real message"
|
||||||
|
Assert-True (-not [bool]$result.uploaded_real_file) "probe must not upload a real file"
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
ok = $true
|
||||||
|
script = $scriptPath
|
||||||
|
hwnd = $result.hwnd
|
||||||
|
root_automation_id = $result.root_automation_id
|
||||||
|
send_editor_found = $result.send_editor_found
|
||||||
|
send_button_found = $result.send_button_found
|
||||||
|
probe_click_marker_found = $result.probe_click_marker_found
|
||||||
|
} | ConvertTo-Json -Depth 4 -Compress
|
||||||
Reference in New Issue
Block a user