Files

84 lines
3.3 KiB
C#

using System.Text.Json;
using FlaUI.Core.AutomationElements;
using FlaUI.UIA3;
using WxAgent.Core;
namespace WxAgent.Windows;
public static class WechatUiInspector
{
public static async Task<UiNodeSnapshot> CaptureAsync(string outputPath, CancellationToken cancellationToken, string? windowTitle = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(outputPath);
cancellationToken.ThrowIfCancellationRequested();
using var automation = new UIA3Automation();
var window = (windowTitle is null ? WechatDoctor.FindWechatWindow(automation)
: WechatDoctor.FindNamedWechatWindow(automation, windowTitle, cancellationToken))
?? throw new WxAgentException(WxAgentErrorCode.WindowNotFound, "WeChat window was not found in the current interactive session.");
var guard = new UiTraversalGuard();
var snapshot = Capture(window, cancellationToken, guard, 0);
var fullPath = Path.GetFullPath(outputPath);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
await using var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(stream, snapshot, new JsonSerializerOptions { WriteIndented = true, MaxDepth = 128 }, cancellationToken);
if (guard.Truncated || guard.CycleDetected)
throw new WxAgentException(WxAgentErrorCode.UiStructureChanged, "UI snapshot was saved partially: traversal limit or repeated runtime ID detected.");
return snapshot;
}
private static UiNodeSnapshot Capture(
AutomationElement element,
CancellationToken cancellationToken,
UiTraversalGuard guard,
int depth)
{
cancellationToken.ThrowIfCancellationRequested();
var identity = SafeRead(() => string.Join(',', element.Properties.RuntimeId.ValueOrDefault ?? []));
var canDescend = guard.TryVisit(identity, depth);
var children = new List<UiNodeSnapshot>();
if (canDescend)
{
AutomationElement[] rawChildren;
try { rawChildren = element.FindAllChildren(); }
catch { rawChildren = []; }
foreach (var child in rawChildren)
{
if (guard.Truncated) break;
children.Add(Capture(child, cancellationToken, guard, depth + 1));
}
}
string bounds;
try
{
var rectangle = element.BoundingRectangle;
bounds = $"{rectangle.Left:0},{rectangle.Top:0},{rectangle.Width:0},{rectangle.Height:0}";
}
catch
{
bounds = string.Empty;
}
return new UiNodeSnapshot(
SafeRead(() => element.ControlType.ToString()),
UiSnapshotSanitizer.SanitizeName(SafeRead(() => element.Name)),
UiSnapshotSanitizer.SanitizeAutomationId(SafeRead(() => element.AutomationId)),
SafeRead(() => element.IsEnabled, false),
bounds,
children);
}
private static string SafeRead(Func<string> read)
{
try { return read() ?? string.Empty; } catch { return string.Empty; }
}
private static T SafeRead<T>(Func<T> read, T fallback)
{
try { return read(); } catch { return fallback; }
}
}