84 lines
3.3 KiB
C#
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; }
|
|
}
|
|
}
|