tools: open real offline frmP2PChat window

This commit is contained in:
zhaoyilun
2026-07-11 20:58:26 +08:00
parent a06fe60eb2
commit 811f760efe
5 changed files with 935 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
# A-route 离线打开真实 frmP2PChat 会话窗口
日期2026-07-11
分支:`codex/a-route-open-real-chat-window`
目标:不登录、不连服务端,在本机离线样本中打开真实 `IMPP.Client.Business.ChatManager.SingleChat.frmP2PChat` 窗口,供后续 RPA 对真实窗口做 UIA 控制。
## 结论
已经在未登录/离线环境下打开真实 `frmP2PChat`
成功命令:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\open-offline-real-frmP2PChat.ps1 -RuntimeMode Deps -ShowWindow -HoldSeconds 1
```
成功产物:
- 结果 JSON`E:\coding\codex\isphere-ai-bridge\runs\offline-chat-window\real-frmP2PChat-result-20260711-205312.json`
- HWND`0xC0CB4`
- 截图:`E:\coding\codex\isphere-ai-bridge\runs\offline-chat-window\real-frmP2PChat-20260711-205312.png`
- UIA dump`E:\coding\codex\isphere-ai-bridge\runs\offline-chat-window\uia-dump-real-frmP2PChat-20260711-205312.json`
- UIA root
- `automation_id = frmP2PChat`
- `control_type = Window`
- `framework_id = WinForm`
- `visible = true`
UIA dump 已确认可见关键控件:
- `rtbRecvMessage`
- `rtbSendMessage`
- `btnSend`
窗口内显示“您已处于离线状态,无法发送消息,请上线后再次尝试!”。这是预期状态;本轮目标不是发送消息,也不是上传文件,只是打开真实会话窗口。
## 新增脚本
### 1. 真实窗口打开脚本
`scripts/open-offline-real-frmP2PChat.ps1`
关键能力:
- 自动切到 32-bit PowerShell避免 x86 客户端程序集 `BadImageFormatException`
- 只读加载真实客户端目录:
`runs/offline-real-client-window/full/zyl/Impp`
- 使用 `AssemblyResolve` 从真实客户端目录解析依赖。
- 构造真实类型:
`IMPP.Client.Business.ChatManager.SingleChat.frmP2PChat`
- 支持不 Show 的构造验证,也支持 `-ShowWindow` 真实显示、截图和 UIA dump。
- 捕获 WinForms `ThreadException`,输出 IL offset、堆栈和 JSON 证据,避免 .NET 异常弹窗卡死。
### 2. 构造函数 IL / NullRisk 提取脚本
`scripts/extract-frmP2PChat-il-risk.ps1`
输出:
- 完整构造函数 IL
`runs/offline-chat-window/frmP2PChat-ctor.il.txt`
- `callvirt` / `ldfld` NullRisk CSV
`runs/offline-chat-window/frmP2PChat-ctor-null-risk.csv`
- 风险摘要:
`runs/offline-chat-window/frmP2PChat-ctor-null-risk.md`
提取结果:
- 构造函数源 IL 行:`406271-406860`
- 构造函数行数:`590`
- `callvirt` / `ldfld` 风险项:`150`
## 关键 IL 证据
构造函数签名:
```text
frmP2PChat(com.vision.smack.Chat chat, string pluginInfo, string extendTabJson)
```
高信号 NullRisk
| IL | 证据 | 需要补的运行态 |
|---|---|---|
| `IL_00f6` | `XMPPConnection::add_OnConnectError` | `IMPPManager.Instance.Connection` / backing field |
| `IL_0111` | `XMPPConnection::add_OnReConnectOk` | 同上 |
| `IL_013b` / `IL_0140` | `P2PChat::get_OtherJid()` / `Jid::getBareJid()` | `P2PChat.OtherJid` |
| `IL_0156` | `IMPPManager::get_UserInfo()` | `IMPPManager._userInfo` |
| `IL_0575` | `MessageCenter::add_OnTcpMessageArrived` | `IMPPManager.MessageCenter` |
| `IL_0614` | `RosterManager::UpdateStrangerStatus` | `IMPPManager.RosterManager` |
| `IL_0631` | `BaseConfig::VisualPhoneEnable()` | `Config.BaseConfig` |
Show 阶段新增证据:
| IL | 证据 | 处理 |
|---|---|---|
| `frmP2PChat_Load IL_004d/0058` | `Config::get_BaseConfig()``BaseConfig::RemoteControlVisible()` | 补 `Config.<BaseConfig>k__BackingField` |
| `frmChatBase_Activated IL_0000/0005` | `IMPPManager.get_Instance().get_frmMain().BringModalFormToFront()` | 补 `IMPPManager.<frmMain>k__BackingField` |
| `frmP2PChat_Load IL_137f` | `UserInfoManager.getUserInfo(otherBareJid,true)` 后使用 `UserInfo::get_CompanyID()` | 构造完成后预热 `UserInfoCache` |
注意:`UserInfoCache` 不能在 `frmP2PChat` 构造前预热。过早预热会让构造函数进入 `UCUserInformation.LoadUserInfo(UserInfo)`,在 `UCUserInformation::LoadUserInfo IL_0085` 触发新的 NRE。当前脚本是在真实窗体构造完成后、`Show()` 前预热缓存。
## 最小 fake runtime 清单
当前成功路径最小补齐如下:
1. `IMPPManager.Instance`
- `<MessageCenter>k__BackingField`
- `<frmMain>k__BackingField`
- `LogonUser`
- `_userInfo`
- `_rosterUserInfoList`
2. `SmarkManager`
- `<SmarkManagerInstance>k__BackingField`
3. `XMPPConnection`
- `Jid`
- `UserName`
- `LoginState`
- `CompanyID`
- `Resource`
- `_packetListeners`
- `_packetCollectors`
4. `IMPPManager` inherited manager fields/properties
- `Connection` and `<Connection>k__BackingField`
- `IsConnected` and `<IsConnected>k__BackingField`
- `PresenceManager` and backing field
- `P2PChatManager` and backing field
- `RosterManager` and backing field
5. Config
- `ConfigSystemManager.config`
- `Config.<BaseConfig>k__BackingField`
- `Config.<UserInfo>k__BackingField`
- `Config.<LoginInfo>k__BackingField`
6. `UserInfoCache`
-`frmP2PChat` 构造后、`Show()` 前加入自己和对端的 JID/ID 映射。
7. Chat 对象
- 真实 `com.vision.smack.P2PChat`
- `OtherJid = codex-probe@offline.local`
## 验证命令
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\test-extract-frmP2PChat-il-risk.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\extract-frmP2PChat-il-risk.ps1 -OutDir runs/offline-chat-window
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\test-open-offline-real-frmP2PChat-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\open-offline-real-frmP2PChat.ps1 -RuntimeMode Deps
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\open-offline-real-frmP2PChat.ps1 -RuntimeMode Deps -ShowWindow -HoldSeconds 1
```
## 当前边界
- 已成功打开真实 `frmP2PChat`
- UIA 可 dump 到真实 `frmP2PChat``rtbRecvMessage``rtbSendMessage``btnSend`
- 离线环境下窗口会提示无法发送消息,这是客户端真实离线状态表现,不影响后续 RPA 窗口定位。
- 本轮没有处理真实发送、真实上传、真实登录态。
- `btnSendFile` 在这次真实离线 UIA dump 中没有以 `btnSendFile` automation id 暴露;后续如果 RPA 需要上传文件,需要另开节点定位真实工具栏按钮或菜单路径。
## 下一轮建议
1. 把 A-route RPA 的窗口定位目标从 synthetic probe 切到真实 `frmP2PChat`
- root `automation_id=frmP2PChat`
- send box `automation_id=rtbSendMessage`
- send button `automation_id=btnSend`
2. 如果弹出“企业信息获取失败”影响自动化,单独加一个非阻塞关闭弹窗步骤。
3. 如果后续需要文件按钮,再专门分析真实工具栏图标按钮、菜单项和 `ToolStrip` 命中路径。

View File

@@ -0,0 +1,118 @@
param(
[string]$IlFile = "runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il/IMPlatformClient.exe.il",
[string]$OutDir = "runs/offline-chat-window"
)
$ErrorActionPreference = "Stop"
$repo = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")
function Resolve-RepoPath([string]$PathValue) {
if ([System.IO.Path]::IsPathRooted($PathValue)) {
return $PathValue
}
return (Join-Path $repo $PathValue)
}
$ilFull = Resolve-RepoPath $IlFile
$outFull = Resolve-RepoPath $OutDir
if (-not (Test-Path -LiteralPath $ilFull)) {
throw "IL file not found: $ilFull"
}
New-Item -ItemType Directory -Force -Path $outFull | Out-Null
$lines = Get-Content -LiteralPath $ilFull
$endIndex = $null
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match 'end of method frmP2PChat::\.ctor') {
$endIndex = $i
break
}
}
if ($null -eq $endIndex) {
throw "frmP2PChat::.ctor end marker not found"
}
$startIndex = $null
for ($i = $endIndex; $i -ge 0; $i--) {
if ($lines[$i] -match '^\s*\.method\s+public\s+hidebysig\s+specialname\s+rtspecialname') {
$startIndex = $i
break
}
}
if ($null -eq $startIndex) {
throw "frmP2PChat::.ctor method start not found before line $($endIndex + 1)"
}
$ctorIlFile = Join-Path $outFull "frmP2PChat-ctor.il.txt"
$riskCsvFile = Join-Path $outFull "frmP2PChat-ctor-null-risk.csv"
$riskMdFile = Join-Path $outFull "frmP2PChat-ctor-null-risk.md"
$ctorLines = New-Object System.Collections.Generic.List[string]
for ($i = $startIndex; $i -le $endIndex; $i++) {
$ctorLines.Add(("{0}: {1}" -f ($i + 1), $lines[$i]))
}
$ctorLines | Set-Content -LiteralPath $ctorIlFile -Encoding UTF8
$riskRows = New-Object System.Collections.Generic.List[object]
foreach ($entry in $ctorLines) {
if ($entry -notmatch '\b(callvirt|ldfld)\b') {
continue
}
$offset = $null
if ($entry -match '(IL_[0-9a-fA-F]{4})') {
$offset = $matches[1].ToLowerInvariant().Replace("il_", "IL_")
}
$op = if ($entry -match '\b(callvirt|ldfld)\b') { $matches[1] } else { $null }
$target = ($entry -replace '^.*?\b(callvirt|ldfld)\b\s+', '').Trim()
$riskRows.Add([pscustomobject]@{
source_line = [int]($entry -replace '^(\d+):.*$', '$1')
il_offset = $offset
opcode = $op
target = $target
null_risk = if ($op -eq "callvirt") { "receiver_null_possible" } else { "instance_field_owner_null_possible" }
evidence = $entry.Trim()
})
}
$riskRows | Sort-Object source_line | Export-Csv -LiteralPath $riskCsvFile -NoTypeInformation -Encoding UTF8
$topEvidence = $riskRows |
Where-Object {
$_.target -match 'Connection::add_OnConnectError|Connection::add_OnReConnectOk|P2PChat::get_OtherJid|Jid::getBareJid|IMPPManager::get_UserInfo|UCChatSendMessageBox::GetChatRichTextBox|UCChatSendMessageBox::GetReceiptCheckBox|MessageCenter::add_OnTcpMessageArrived|RosterManager::UpdateStrangerStatus|BaseConfig::VisualPhoneEnable'
} |
Select-Object -First 40
$md = New-Object System.Collections.Generic.List[string]
$md.Add("# frmP2PChat constructor IL NullRisk")
$md.Add("")
$md.Add("- Source IL: $ilFull")
$md.Add("- Constructor source lines: $($startIndex + 1)-$($endIndex + 1)")
$md.Add("- Constructor line count: $($ctorLines.Count)")
$md.Add("- callvirt / ldfld risk rows: $($riskRows.Count)")
$md.Add("")
$md.Add("## High-signal risks")
$md.Add("")
$md.Add("| source_line | il_offset | opcode | target |")
$md.Add("|---:|---|---|---|")
foreach ($row in $topEvidence) {
$target = ($row.target -replace '\|', '\\|')
$md.Add("| $($row.source_line) | $($row.il_offset) | $($row.opcode) | $target |")
}
$md.Add("")
$md.Add("## Files")
$md.Add("")
$md.Add("- Full constructor IL: $ctorIlFile")
$md.Add("- Full NullRisk CSV: $riskCsvFile")
$md | Set-Content -LiteralPath $riskMdFile -Encoding UTF8
[pscustomobject]@{
ok = $true
il_file = $ilFull
ctor_line_start = $startIndex + 1
ctor_line_end = $endIndex + 1
ctor_line_count = $ctorLines.Count
risk_count = $riskRows.Count
ctor_il_file = $ctorIlFile
risk_csv_file = $riskCsvFile
risk_md_file = $riskMdFile
} | ConvertTo-Json -Compress

View File

@@ -0,0 +1,545 @@
param(
[string]$ClientDir = "runs/offline-real-client-window/full/zyl/Impp",
[string]$OutDir = "runs/offline-chat-window",
[ValidateSet("Minimal", "Deps")]
[string]$RuntimeMode = "Deps",
[switch]$SelfChat,
[switch]$SkipRosterManager,
[switch]$ShowWindow,
[string]$HelperExe = "runs/win-helper/ISphereWinHelper.exe",
[switch]$NoScreenshot,
[int]$HoldSeconds = 8,
[int]$X = 180,
[int]$Y = 180,
[int]$Width = 900,
[int]$Height = 680
)
$ErrorActionPreference = "Stop"
if ([Environment]::Is64BitProcess) {
$x86PowerShell = Join-Path $env:WINDIR "SysWOW64\WindowsPowerShell\v1.0\powershell.exe"
if (-not (Test-Path -LiteralPath $x86PowerShell)) {
throw "32-bit PowerShell not found: $x86PowerShell"
}
$relayArgs = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath)
foreach ($entry in $PSBoundParameters.GetEnumerator()) {
$name = [string]$entry.Key
$value = $entry.Value
if ($value -is [System.Management.Automation.SwitchParameter]) {
if ($value.IsPresent) {
$relayArgs += "-$name"
}
}
else {
$relayArgs += "-$name"
$relayArgs += [string]$value
}
}
& $x86PowerShell @relayArgs
exit $LASTEXITCODE
}
$repo = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")
function Resolve-RepoPath([string]$PathValue) {
if ([System.IO.Path]::IsPathRooted($PathValue)) {
return $PathValue
}
return (Join-Path $repo $PathValue)
}
function Get-TypeMemberInHierarchy([Type]$Type, [string]$Name, [string]$Kind) {
$flags = [System.Reflection.BindingFlags]"Public,NonPublic,Instance,Static"
$cursor = $Type
while ($null -ne $cursor) {
if ($Kind -eq "Property") {
$member = $cursor.GetProperty($Name, $flags)
}
else {
$member = $cursor.GetField($Name, $flags)
}
if ($member) {
return $member
}
$cursor = $cursor.BaseType
}
return $null
}
function Set-PropInHierarchy([object]$Object, [Type]$Type, [string]$Name, [object]$Value) {
$prop = Get-TypeMemberInHierarchy -Type $Type -Name $Name -Kind "Property"
if ($prop -and $prop.CanWrite) {
$target = if ($prop.GetSetMethod($true).IsStatic) { $null } else { $Object }
$prop.SetValue($target, $Value, $null)
return $true
}
return $false
}
function Set-FieldInHierarchy([object]$Object, [Type]$Type, [string]$Name, [object]$Value) {
$field = Get-TypeMemberInHierarchy -Type $Type -Name $Name -Kind "Field"
if ($field) {
$target = if ($field.IsStatic) { $null } else { $Object }
$field.SetValue($target, $Value)
return $true
}
return $false
}
function New-ByConstructor([Type]$Type, [Type[]]$ParameterTypes, [object[]]$Arguments) {
$ctor = $Type.GetConstructor($ParameterTypes)
if (-not $ctor) {
$sig = ($ParameterTypes | ForEach-Object { $_.FullName }) -join ", "
throw "constructor not found: $($Type.FullName)($sig)"
}
return $ctor.Invoke($Arguments)
}
function Convert-ExceptionToRecord([Exception]$Exception) {
$records = @()
$cursor = $Exception
$depth = 0
while ($cursor) {
$trace = New-Object System.Diagnostics.StackTrace($cursor, $true)
$frames = @()
for ($i = 0; $i -lt $trace.FrameCount; $i++) {
$frame = $trace.GetFrame($i)
$method = $frame.GetMethod()
$offset = $frame.GetILOffset()
$frames += [pscustomobject]@{
index = $i
method_type = if ($method.DeclaringType) { $method.DeclaringType.FullName } else { $null }
method_name = $method.Name
il_offset = $offset
il_offset_hex = if ($offset -ge 0) { "IL_{0:x4}" -f $offset } else { $null }
native_offset = $frame.GetNativeOffset()
file = $frame.GetFileName()
line = $frame.GetFileLineNumber()
}
}
$records += [pscustomobject]@{
depth = $depth
type = $cursor.GetType().FullName
message = $cursor.Message
target_site = if ($cursor.TargetSite) { "$($cursor.TargetSite.DeclaringType.FullName)::$($cursor.TargetSite.Name)" } else { $null }
frames = $frames
}
$cursor = $cursor.InnerException
$depth++
}
return $records
}
function Get-FirstRelevantFrame([object[]]$ExceptionRecords) {
foreach ($record in $ExceptionRecords) {
foreach ($frame in $record.frames) {
if ($frame.method_type -and
$frame.method_type -notlike "System.*" -and
$frame.method_type -notlike "Microsoft.*" -and
$frame.method_type -notlike "MS.*") {
return $frame
}
}
}
return $null
}
function Find-IlLine([string]$OutDirFull, [string]$IlOffsetHex) {
if ([string]::IsNullOrWhiteSpace($IlOffsetHex)) {
return $null
}
$ctorIl = Join-Path $OutDirFull "frmP2PChat-ctor.il.txt"
if (-not (Test-Path -LiteralPath $ctorIl)) {
return $null
}
$match = Select-String -LiteralPath $ctorIl -Pattern $IlOffsetHex -SimpleMatch | Select-Object -First 1
if ($match) {
return @{
path = $ctorIl
line_number = $match.LineNumber
text = $match.Line.Trim()
}
}
return $null
}
function Invoke-HelperJson([string]$HelperPath, [string]$Op, [hashtable]$OpArgs, [string]$RequestId) {
$request = @{
protocol = "isphere.helper.v1"
request_id = $RequestId
op = $Op
timeout_ms = 5000
args = $OpArgs
}
$json = $request | ConvertTo-Json -Depth 16 -Compress
$output = $json | & $HelperPath --json
try {
return $output | ConvertFrom-Json
}
catch {
throw "helper output was not JSON: $output"
}
}
function Save-WindowScreenshot([string]$OutputPath, [int]$ScreenX, [int]$ScreenY, [int]$CaptureWidth, [int]$CaptureHeight) {
$bitmap = New-Object System.Drawing.Bitmap $CaptureWidth, $CaptureHeight
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
try {
$graphics.CopyFromScreen($ScreenX, $ScreenY, 0, 0, $bitmap.Size)
$bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally {
$graphics.Dispose()
$bitmap.Dispose()
}
}
function New-UserInfo([Type]$UserInfoType, [string]$Id, [string]$Jid, [string]$Name, [string]$CompanyID) {
$user = [Activator]::CreateInstance($UserInfoType)
foreach ($kv in @(
@("Id", $Id),
@("Jid", $Jid),
@("LoginName", $Id),
@("Domain", "offline.local"),
@("Name", $Name),
@("CompanyID", $CompanyID),
@("DeptID", "codex-dept"),
@("DeptName", "Codex Dept"),
@("Enable", "1"),
@("chatAble", "1"),
@("Mobile", ""),
@("Tel", ""),
@("Email", "")
)) {
Set-PropInHierarchy -Object $user -Type $UserInfoType -Name $kv[0] -Value $kv[1] | Out-Null
}
return $user
}
$clientFull = Resolve-RepoPath $ClientDir
$outFull = Resolve-RepoPath $OutDir
$helperFull = Resolve-RepoPath $HelperExe
New-Item -ItemType Directory -Force -Path $outFull | Out-Null
if (-not (Test-Path -LiteralPath (Join-Path $clientFull "IMPlatformClient.exe"))) {
throw "IMPlatformClient.exe not found under $clientFull"
}
Set-Location -LiteralPath $clientFull
[AppDomain]::CurrentDomain.add_AssemblyResolve({
param($sender, $eventArgs)
$assemblyName = New-Object System.Reflection.AssemblyName($eventArgs.Name)
foreach ($extension in @(".dll", ".exe")) {
$candidate = Join-Path $script:ClientDirectoryForResolve ($assemblyName.Name + $extension)
if (Test-Path -LiteralPath $candidate) {
return [System.Reflection.Assembly]::LoadFrom($candidate)
}
}
return $null
})
$script:ClientDirectoryForResolve = $clientFull
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$resultFile = Join-Path $outFull ("real-frmP2PChat-result-{0}.json" -f $timestamp)
try {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$script:RealFrmP2PThreadExceptions = New-Object System.Collections.ArrayList
[System.Windows.Forms.Application]::SetUnhandledExceptionMode([System.Windows.Forms.UnhandledExceptionMode]::CatchException)
[System.Windows.Forms.Application]::add_ThreadException({
param($sender, $eventArgs)
[void]$script:RealFrmP2PThreadExceptions.Add($eventArgs.Exception)
})
$asm = [System.Reflection.Assembly]::LoadFrom((Join-Path $clientFull "IMPlatformClient.exe"))
$smack = [System.Reflection.Assembly]::LoadFrom((Join-Path $clientFull "smack.dll"))
$common = [System.Reflection.Assembly]::LoadFrom((Join-Path $clientFull "IMPP.Common.dll"))
$mgrType = $asm.GetType("IMPP.Client.IMPPManager", $true)
$mcType = $asm.GetType("IMPP.Client.control.MessageCenter", $true)
$configType = $asm.GetType("IMPP.Client.Config", $true)
$baseConfigType = $asm.GetType("IMPP.Client.BaseConfig", $true)
$configMgrType = $asm.GetType("IMPP.Client.ConfigSystemManager", $true)
$configModelType = $asm.GetType("IMPP.Client.ConfigSystemModel", $true)
$userInfoCacheType = $asm.GetType("IMPP.Client.Framework.Configuration.UserInfoCache", $true)
$loginInfoType = $asm.GetType("IMPP.Client.LoginInfo", $true)
$p2pFormType = $asm.GetType("IMPP.Client.Business.ChatManager.SingleChat.frmP2PChat", $true)
$chatBaseType = $asm.GetType("IMPP.Client.Forms.frmChatBase", $true)
$frmMainType = $asm.GetType("IMPP.Client.frmMain", $true)
$connType = $smack.GetType("com.vision.smack.XMPPConnection", $true)
$jidType = $smack.GetType("com.vision.smack.Jid", $true)
$chatType = $smack.GetType("com.vision.smack.Chat", $true)
$p2pChatType = $smack.GetType("com.vision.smack.P2PChat", $true)
$rosterType = $smack.GetType("com.vision.smack.RosterManager", $true)
$presenceType = $smack.GetType("com.vision.smack.PresenceManager", $true)
$p2pMgrType = $smack.GetType("com.vision.smack.P2PChatManager", $true)
$bookMarkType = $smack.GetType("com.vision.smack.muc.BookMarkManager", $true)
$packetCollectorType = $smack.GetType("com.vision.smack.PacketCollector", $true)
$userInfoType = $common.GetType("IMPP.Common.UserInfo", $true)
$mgr = $mgrType.GetProperty("Instance", [System.Reflection.BindingFlags]"Public,Static").GetValue($null, $null)
$fakeMessageCenter = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($mcType)
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "<MessageCenter>k__BackingField" -Value $fakeMessageCenter | Out-Null
$fakeFrmMain = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($frmMainType)
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "<frmMain>k__BackingField" -Value $fakeFrmMain | Out-Null
$login = [Activator]::CreateInstance($loginInfoType)
Set-PropInHierarchy -Object $login -Type $loginInfoType -Name "Trait" -Value "codex_probe" | Out-Null
Set-PropInHierarchy -Object $login -Type $loginInfoType -Name "TrueTraid" -Value "codex_probe" | Out-Null
Set-PropInHierarchy -Object $login -Type $loginInfoType -Name "Status" -Value "online" | Out-Null
Set-PropInHierarchy -Object $mgr -Type $mgrType -Name "LogonUser" -Value $login | Out-Null
$companyId = "codex-company"
$otherBareJid = "codex-probe@offline.local"
$myBareJid = if ($SelfChat) { $otherBareJid } else { "codex-user@offline.local" }
$myName = if ($SelfChat) { "Codex Probe" } else { "Codex User" }
$jidCtor3 = $jidType.GetConstructor(@([string], [string], [string]))
$myJid = $jidCtor3.Invoke(@(($myBareJid -split "@")[0], "offline.local", "pc"))
$fakeConn = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($connType)
Set-PropInHierarchy -Object $fakeConn -Type $connType -Name "Jid" -Value $myJid | Out-Null
Set-PropInHierarchy -Object $fakeConn -Type $connType -Name "UserName" -Value (($myBareJid -split "@")[0]) | Out-Null
Set-PropInHierarchy -Object $fakeConn -Type $connType -Name "LoginState" -Value "online" | Out-Null
Set-PropInHierarchy -Object $fakeConn -Type $connType -Name "CompanyID" -Value $companyId | Out-Null
Set-PropInHierarchy -Object $fakeConn -Type $connType -Name "Resource" -Value "pc" | Out-Null
Set-FieldInHierarchy -Object $fakeConn -Type $connType -Name "_packetListeners" -Value (New-Object System.Collections.ArrayList) | Out-Null
$packetCollectorListType = [System.Collections.Generic.List``1].MakeGenericType($packetCollectorType)
Set-FieldInHierarchy -Object $fakeConn -Type $connType -Name "_packetCollectors" -Value ([Activator]::CreateInstance($packetCollectorListType)) | Out-Null
Set-PropInHierarchy -Object $mgr -Type $mgrType -Name "Connection" -Value $fakeConn | Out-Null
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "<Connection>k__BackingField" -Value $fakeConn | Out-Null
Set-PropInHierarchy -Object $mgr -Type $mgrType -Name "IsConnected" -Value $true | Out-Null
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "<IsConnected>k__BackingField" -Value $true | Out-Null
Set-FieldInHierarchy -Object $null -Type ($smack.GetType("com.vision.smack.SmarkManager", $true)) -Name "<SmarkManagerInstance>k__BackingField" -Value $mgr | Out-Null
if ($RuntimeMode -eq "Deps") {
$myUser = New-UserInfo -UserInfoType $userInfoType -Id (($myBareJid -split "@")[0]) -Jid $myBareJid -Name $myName -CompanyID $companyId
$otherUser = New-UserInfo -UserInfoType $userInfoType -Id "codex-probe" -Jid $otherBareJid -Name "Codex Probe" -CompanyID $companyId
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "_userInfo" -Value $myUser | Out-Null
$stringDictType = [System.Collections.Generic.Dictionary``2].MakeGenericType([string], [string])
$baseConfigDict = [Activator]::CreateInstance($stringDictType, [StringComparer]::OrdinalIgnoreCase)
foreach ($kv in @(
@("Remote_Control_Visible", "false"),
@("VisualPhone_enable", "false"),
@("showacctno", "no"),
@("ShowDepartment", "NO"),
@("StrucPPLNo", "NO"),
@("MaintenanceAssistant", ""),
@("CustomerServiceSuffix", ""),
@("CustomerCN", ""),
@("CustomerPhone", "")
)) {
$baseConfigDict[$kv[0]] = $kv[1]
$baseConfigDict[$kv[0].ToLowerInvariant()] = $kv[1]
}
$baseConfigRawDict = $baseConfigDict.PSObject.BaseObject
$baseConfigCtor = $baseConfigType.GetConstructor(@($baseConfigRawDict.GetType()))
if (-not $baseConfigCtor) {
throw "constructor not found: $($baseConfigType.FullName)($($baseConfigRawDict.GetType().FullName))"
}
$baseConfig = $baseConfigCtor.Invoke([object[]]@($baseConfigRawDict))
Set-FieldInHierarchy -Object $null -Type $configType -Name "<BaseConfig>k__BackingField" -Value $baseConfig | Out-Null
Set-FieldInHierarchy -Object $null -Type $configType -Name "<UserInfo>k__BackingField" -Value $myUser | Out-Null
Set-FieldInHierarchy -Object $null -Type $configType -Name "<LoginInfo>k__BackingField" -Value $login | Out-Null
$userListType = [System.Collections.Generic.List``1].MakeGenericType($userInfoType)
$rosterUsers = [Activator]::CreateInstance($userListType)
$rosterUsers.Add($myUser) | Out-Null
$rosterUsers.Add($otherUser) | Out-Null
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "_rosterUserInfoList" -Value $rosterUsers | Out-Null
$cfg = [Activator]::CreateInstance($configModelType)
foreach ($kv in @(
@("Font", "Microsoft YaHei UI"),
@("Size", [single]9),
@("FontStyle", "Regular"),
@("Color", "Black"),
@("SendMsgKeyPress", "Enter"),
@("SendFilePath", $outFull),
@("SendFileSize", 10),
@("SendFileConf", $false),
@("AllEnable", $true)
)) {
Set-PropInHierarchy -Object $cfg -Type $configModelType -Name $kv[0] -Value $kv[1] | Out-Null
}
$configMgrType.GetField("config", [System.Reflection.BindingFlags]"NonPublic,Static").SetValue($null, $cfg)
$presence = New-ByConstructor -Type $presenceType -ParameterTypes @($connType) -Arguments @($fakeConn)
Set-PropInHierarchy -Object $mgr -Type $mgrType -Name "PresenceManager" -Value $presence | Out-Null
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "<PresenceManager>k__BackingField" -Value $presence | Out-Null
$bookmark = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($bookMarkType)
$p2pManager = New-ByConstructor -Type $p2pMgrType -ParameterTypes @($connType, $bookMarkType) -Arguments @($fakeConn, $bookmark)
Set-PropInHierarchy -Object $mgr -Type $mgrType -Name "P2PChatManager" -Value $p2pManager | Out-Null
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "<P2PChatManager>k__BackingField" -Value $p2pManager | Out-Null
if (-not $SkipRosterManager) {
$roster = New-ByConstructor -Type $rosterType -ParameterTypes @($connType) -Arguments @($fakeConn)
Set-PropInHierarchy -Object $mgr -Type $mgrType -Name "RosterManager" -Value $roster | Out-Null
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "<RosterManager>k__BackingField" -Value $roster | Out-Null
}
else {
Set-PropInHierarchy -Object $mgr -Type $mgrType -Name "RosterManager" -Value $null | Out-Null
Set-FieldInHierarchy -Object $mgr -Type $mgrType -Name "<RosterManager>k__BackingField" -Value $null | Out-Null
}
}
$p2pChat = New-ByConstructor -Type $p2pChatType -ParameterTypes @($connType, [string]) -Arguments @($fakeConn, $otherBareJid)
$otherJid = $p2pChatType.GetProperty("OtherJid").GetValue($p2pChat, $null)
if ($null -eq $otherJid) {
$otherJid = $jidCtor3.Invoke(@("codex-probe", "offline.local", ""))
Set-FieldInHierarchy -Object $p2pChat -Type $p2pChatType -Name "<OtherJid>k__BackingField" -Value $otherJid | Out-Null
}
$formCtor = $p2pFormType.GetConstructor(@($chatType, [string], [string]))
$form = $formCtor.Invoke(@($p2pChat, "", ""))
if ($RuntimeMode -eq "Deps") {
$userInfoCache = $userInfoCacheType.GetMethod("GetInstance", [System.Reflection.BindingFlags]"Public,Static").Invoke($null, @())
$addUserInfoCache = $userInfoCacheType.GetMethod("AddCache", [Type[]]@([string], $userInfoType))
foreach ($cacheEntry in @(
@{ Key = $myBareJid; User = $myUser },
@{ Key = $myBareJid.ToLowerInvariant(); User = $myUser },
@{ Key = (($myBareJid -split "@")[0]); User = $myUser },
@{ Key = $otherBareJid; User = $otherUser },
@{ Key = $otherBareJid.ToLowerInvariant(); User = $otherUser },
@{ Key = "codex-probe"; User = $otherUser }
)) {
$addUserInfoCache.Invoke($userInfoCache, [object[]]@([string]$cacheEntry.Key, $cacheEntry.User.PSObject.BaseObject))
}
}
$form.Name = "frmP2PChat"
$form.Text = "Codex Real frmP2PChat Offline Probe"
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
$form.Left = $X
$form.Top = $Y
$form.Width = $Width
$form.Height = $Height
$uiaDumpFile = $null
$uiaRoot = $null
$screenshotFile = $null
if ($ShowWindow) {
$form.Show()
[System.Windows.Forms.Application]::DoEvents()
Start-Sleep -Milliseconds 800
[System.Windows.Forms.Application]::DoEvents()
if ($script:RealFrmP2PThreadExceptions.Count -gt 0) {
$threadExceptionRecords = @()
foreach ($threadException in $script:RealFrmP2PThreadExceptions) {
$threadExceptionRecords += Convert-ExceptionToRecord -Exception $threadException
}
$relevantFrame = Get-FirstRelevantFrame -ExceptionRecords $threadExceptionRecords
$threadResult = [pscustomobject]@{
ok = $false
stage = "show_thread_exception"
process_bitness = 32
runtime_mode = $RuntimeMode
self_chat = [bool]$SelfChat
skip_roster_manager = [bool]$SkipRosterManager
client_dir = $clientFull
type = $p2pFormType.FullName
hwnd = if ($form.IsHandleCreated) { "0x" + $form.Handle.ToInt64().ToString("X") } else { $null }
visible = $form.Visible
relevant_frame = $relevantFrame
exceptions = $threadExceptionRecords
result_file = $resultFile
}
$threadResult | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $resultFile -Encoding UTF8
$threadResult | ConvertTo-Json -Depth 32 -Compress
try { $form.Close() } catch {}
exit 3
}
if ($form.IsHandleCreated -and (Test-Path -LiteralPath $helperFull)) {
$hwndForDump = "0x" + $form.Handle.ToInt64().ToString("X")
$dump = Invoke-HelperJson -HelperPath $helperFull -Op "dump_uia" -OpArgs @{
hwnd = $hwndForDump
max_depth = 8
max_children = 250
include_text = $true
} -RequestId "real-frmP2PChat-dump-uia"
$uiaDumpFile = Join-Path $outFull ("uia-dump-real-frmP2PChat-{0}.json" -f $timestamp)
$dump | ConvertTo-Json -Depth 40 | Set-Content -LiteralPath $uiaDumpFile -Encoding UTF8
if ($dump.ok) {
$uiaRoot = $dump.data.root
}
}
if (-not $NoScreenshot) {
$screenshotFile = Join-Path $outFull ("real-frmP2PChat-{0}.png" -f $timestamp)
Save-WindowScreenshot -OutputPath $screenshotFile -ScreenX $form.Left -ScreenY $form.Top -CaptureWidth $form.Width -CaptureHeight $form.Height
}
$remainingHold = [Math]::Max(0, $HoldSeconds)
if ($remainingHold -gt 0) {
Start-Sleep -Seconds $remainingHold
}
}
$result = [pscustomobject]@{
ok = $true
process_bitness = 32
runtime_mode = $RuntimeMode
self_chat = [bool]$SelfChat
skip_roster_manager = [bool]$SkipRosterManager
client_dir = $clientFull
type = $p2pFormType.FullName
chat_type = $p2pChat.GetType().FullName
hwnd = if ($form.IsHandleCreated) { "0x" + $form.Handle.ToInt64().ToString("X") } else { $null }
visible = $form.Visible
name = $form.Name
text = $form.Text
size = @{ width = $form.Width; height = $form.Height }
result_file = $resultFile
uia_dump_file = $uiaDumpFile
screenshot_file = $screenshotFile
uia_root = $uiaRoot
}
$result | ConvertTo-Json -Depth 24 | Set-Content -LiteralPath $resultFile -Encoding UTF8
$result | ConvertTo-Json -Depth 24 -Compress
}
catch {
$exceptionRecords = Convert-ExceptionToRecord -Exception $_.Exception
$ctorFrame = $null
foreach ($record in $exceptionRecords) {
foreach ($frame in $record.frames) {
if ($frame.method_type -eq "IMPP.Client.Business.ChatManager.SingleChat.frmP2PChat" -and $frame.method_name -eq ".ctor") {
$ctorFrame = $frame
break
}
}
if ($ctorFrame) { break }
}
$matchedIlLine = if ($ctorFrame) { Find-IlLine -OutDirFull $outFull -IlOffsetHex $ctorFrame.il_offset_hex } else { $null }
$result = [pscustomobject]@{
ok = $false
process_bitness = 32
runtime_mode = $RuntimeMode
self_chat = [bool]$SelfChat
skip_roster_manager = [bool]$SkipRosterManager
client_dir = $clientFull
error = $_.Exception.ToString()
category = $_.CategoryInfo.ToString()
script_stack = $_.ScriptStackTrace
ctor_il_offset_hex = if ($ctorFrame) { $ctorFrame.il_offset_hex } else { $null }
ctor_frame = $ctorFrame
matched_il_line = $matchedIlLine
exceptions = $exceptionRecords
result_file = $resultFile
}
$result | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $resultFile -Encoding UTF8
$result | ConvertTo-Json -Depth 32 -Compress
exit 2
}

View File

@@ -0,0 +1,58 @@
param(
[string]$ScriptPath = "scripts/extract-frmP2PChat-il-risk.ps1",
[string]$OutDir = "runs/offline-chat-window/il-risk-contract"
)
$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"
}
$outFull = if ([System.IO.Path]::IsPathRooted($OutDir)) {
$OutDir
}
else {
Join-Path $repo $OutDir
}
if (Test-Path -LiteralPath $outFull) {
Remove-Item -LiteralPath $outFull -Recurse -Force
}
$raw = & powershell -NoProfile -ExecutionPolicy Bypass -File $target -OutDir $OutDir
$result = $raw | ConvertFrom-Json
if (-not $result.ok) {
throw "extract script returned ok=false"
}
foreach ($path in @($result.ctor_il_file, $result.risk_csv_file, $result.risk_md_file)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "expected output missing: $path"
}
}
if ($result.ctor_line_count -lt 500) {
throw "ctor IL line count too small: $($result.ctor_line_count)"
}
if ($result.risk_count -lt 50) {
throw "risk count too small: $($result.risk_count)"
}
$riskText = Get-Content -LiteralPath $result.risk_csv_file -Raw
foreach ($required in @("IL_00f6", "add_OnConnectError", "P2PChat::get_OtherJid", "IMPPManager::get_UserInfo")) {
if ($riskText -notmatch [regex]::Escape($required)) {
throw "risk csv missing evidence: $required"
}
}
[pscustomobject]@{
ok = $true
checked = 4
ctor_line_count = $result.ctor_line_count
risk_count = $result.risk_count
} | ConvertTo-Json -Compress

View File

@@ -0,0 +1,49 @@
param(
[string]$ScriptPath = "scripts/open-offline-real-frmP2PChat.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 = "x86 PowerShell guard"; Pattern = "Environment]::Is64BitProcess" },
@{ Name = "AssemblyResolve dependency loader"; Pattern = "AssemblyResolve" },
@{ Name = "fake runtime mode parameter"; Pattern = "RuntimeMode" },
@{ Name = "self chat variant"; Pattern = "SelfChat" },
@{ Name = "skip roster variant"; Pattern = "SkipRosterManager" },
@{ Name = "CLR IL offset extraction"; Pattern = "GetILOffset" },
@{ Name = "IL offset JSON field"; Pattern = "il_offset_hex" },
@{ Name = "Config.BaseConfig fake runtime"; Pattern = "<BaseConfig>k__BackingField" },
@{ Name = "remote control config key"; Pattern = "Remote_Control_Visible" },
@{ Name = "fake frmMain activation target"; Pattern = "<frmMain>k__BackingField" },
@{ Name = "UserInfo cache warmup"; Pattern = "UserInfoCache" },
@{ Name = "result artifact output"; Pattern = "real-frmP2PChat-result" }
)
$missing = @()
foreach ($item in $requiredPatterns) {
if ($text -notmatch $item.Pattern) {
$missing += $item.Name
}
}
if ($missing.Count -gt 0) {
throw "contract missing: $($missing -join ', ')"
}
[pscustomobject]@{
ok = $true
script = $target
checked = $requiredPatterns.Count
} | ConvertTo-Json -Compress