using FlaUI.Core.AutomationElements; using FlaUI.Core.Definitions; using WxAgent.Core; namespace WxAgent.Windows; public static partial class WechatChatClient { private static FlaUI.Core.AutomationElements.TextBox GetSearchBox(AutomationElement main) => (main.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit).And(cf.ByName(WechatLocators.Search))) ?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Search input was not found.")).AsTextBox(); private static AutomationElement[] GetSearchRows(AutomationElement main) => (FindByAutomationId(main, WechatLocators.SearchList) ?? throw new WxAgentException(WxAgentErrorCode.UiStructureChanged, "Local search list was not found.")) .FindAllChildren(cf => cf.ByControlType(ControlType.ListItem)); private static WechatSearchPage ReadSearchPage(AutomationElement[] rows, string query, string? category = null) => WechatSearchRules.Parse(rows.Select(row => (SafeAutomationId(row), SafeName(row))).ToArray(), query, category); private static async Task ResetLocalSearchAsync(AutomationElement main, string query, CancellationToken cancellationToken) { GetSearchBox(main).Text = string.Empty; GetSearchBox(main).Text = query; if (!string.Equals(GetSearchBox(main).Text, query, StringComparison.Ordinal)) throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "Search input did not retain the exact query."); await Task.Delay(500, cancellationToken).ConfigureAwait(false); return ReadSearchPage(GetSearchRows(main), query); } private static async Task ExpandLocalCategoryAsync( AutomationElement main, string query, WechatSearchExpansion expansion, CancellationToken cancellationToken) { var rows = GetSearchRows(main); var current = ReadSearchPage(rows, query).Expansions.SingleOrDefault(item => item.Category == expansion.Category) ?? throw new WxAgentException(WxAgentErrorCode.UiStructureChanged, "Local expansion control changed before activation."); // This row was classified under a local header, not found by a global keyword/name search. rows[current.RowIndex].Click(); for (var attempt = 0; attempt < 25; attempt++) { await Task.Delay(100, cancellationToken).ConfigureAwait(false); var page = ReadSearchPage(GetSearchRows(main), query, current.Category); if (page.Results.Count(result => result.Category == current.Category) >= current.ExpectedCount && !page.Expansions.Any(item => item.Category == current.Category)) return page; } throw new WxAgentException(WxAgentErrorCode.UiStructureChanged, "Not all local search results became available; refusing a partial selection."); } private static async Task> SearchLocalSessionsCoreAsync( AutomationElement main, string query, CancellationToken cancellationToken) { var overview = await ResetLocalSearchAsync(main, query, cancellationToken).ConfigureAwait(false); var results = overview.Results.ToList(); foreach (var expansion in overview.Expansions) { await ResetLocalSearchAsync(main, query, cancellationToken).ConfigureAwait(false); var full = await ExpandLocalCategoryAsync(main, query, expansion, cancellationToken).ConfigureAwait(false); results.RemoveAll(result => result.Category == expansion.Category); results.AddRange(full.Results.Where(result => result.Category == expansion.Category)); } return results; } private static async Task OpenLocalSearchSessionAsync(AutomationElement main, string name, CancellationToken cancellationToken, string? query = null) { query ??= name; var results = await SearchLocalSessionsCoreAsync(main, query, cancellationToken).ConfigureAwait(false); var selected = WechatSearchRules.RequireExact(results, name); var page = await ResetLocalSearchAsync(main, query, cancellationToken).ConfigureAwait(false); var expansion = page.Expansions.SingleOrDefault(item => item.Category == selected.Category); if (expansion is not null) await ExpandLocalCategoryAsync(main, query, expansion, cancellationToken).ConfigureAwait(false); var rows = GetSearchRows(main); var current = ReadSearchPage(rows, query, expansion?.Category); var fresh = WechatSearchRules.RequireExact(current.Results, name); if (fresh.Category != selected.Category) throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Local search category changed before selection."); var matches = rows.Where(row => SafeAutomationId(row) == fresh.AutomationId && SessionNameMatches(SafeName(row), name)).ToArray(); if (matches.Length != 1) throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Local search target is no longer unique."); if (matches[0].IsOffscreen) { var scroll = matches[0].Patterns.ScrollItem.PatternOrDefault ?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Local result is offscreen and cannot be scrolled into view."); scroll.ScrollIntoView(); } matches[0].Click(); await WaitForSessionPageAsync(main, name, cancellationToken).ConfigureAwait(false); return fresh; } }