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 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(); 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 read) { try { return read() ?? string.Empty; } catch { return string.Empty; } } private static T SafeRead(Func read, T fallback) { try { return read(); } catch { return fallback; } } }