Files

187 lines
11 KiB
C#

using FlaUI.Core.AutomationElements;
using FlaUI.Core.Input;
using WxAgent.Core;
namespace WxAgent.Windows;
public static partial class WechatChatClient
{
public static Task<WechatSessionScrollResult> ScrollSessionsAsync(WechatScrollDirection direction, int pages = 1, CancellationToken cancellationToken = default) =>
WithMainWindowAsync(async (main, _) =>
{
WechatNavigationRules.ValidateScrollLimit(pages);
if (!Enum.IsDefined(direction))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Unknown scroll direction.");
var before = ReadSessionViewport(main);
var initial = SessionViewportKey(before);
var scrolls = 0;
for (; scrolls < pages;)
{
cancellationToken.ThrowIfCancellationRequested();
ScrollNavigationList(main, WechatLocators.SessionList, direction);
scrolls++;
await Task.Delay(350, cancellationToken).ConfigureAwait(false);
var after = ReadSessionViewport(main);
if (SessionViewportKey(before) == SessionViewportKey(after)) break;
before = after;
}
var sessions = ReadSessionViewport(main);
return new WechatSessionScrollResult(scrolls, initial != SessionViewportKey(sessions), sessions);
}, cancellationToken);
public static Task<IReadOnlyList<ChatMessageSnapshot>> GoToLatestAsync(string session, int maxScrolls = 100, CancellationToken cancellationToken = default) =>
WithMainWindowAsync(async (main, _) =>
{
ArgumentException.ThrowIfNullOrWhiteSpace(session);
WechatNavigationRules.ValidateScrollLimit(maxScrolls);
await OpenNamedSessionAsync(main, session, cancellationToken).ConfigureAwait(false);
await GoToLatestCoreAsync(main, session, maxScrolls, cancellationToken).ConfigureAwait(false);
return ReadVisible(main);
}, cancellationToken);
public static Task<WechatMessagePosition> LocateMessageAsync(string session, string fingerprint, int maxScrolls = 30, CancellationToken cancellationToken = default) =>
WithMainWindowAsync(async (main, _) =>
{
ArgumentException.ThrowIfNullOrWhiteSpace(session);
WechatNavigationRules.ValidateScrollLimit(maxScrolls);
WechatNavigationRules.FindMessage([], fingerprint);
await OpenNamedSessionAsync(main, session, cancellationToken).ConfigureAwait(false);
var previous = "";
var unchanged = 0;
for (var page = 0; page <= maxScrolls; page++)
{
cancellationToken.ThrowIfCancellationRequested();
RequireNavigationSession(main, session);
var visible = ReadVisible(main);
var match = WechatNavigationRules.FindMessage(visible, fingerprint);
if (match is not null)
{
var element = FindVisibleMessageElement(main, match.Fingerprint);
if (element.Patterns.ScrollItem.IsSupported)
ExecuteInputStep("locate-message", () => element.Patterns.ScrollItem.Pattern.ScrollIntoView());
element = FindVisibleMessageElement(main, match.Fingerprint);
var list = FindByAutomationId(main, WechatLocators.MessageList)!;
if (element.Properties.IsOffscreen.ValueOrDefault || !element.BoundingRectangle.IntersectsWith(list.BoundingRectangle))
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The matched message could not be brought into the visible viewport.");
RequireNavigationSession(main, session);
return new WechatMessagePosition(match, page);
}
var signature = string.Join('|', visible.Select(message => message.Fingerprint));
unchanged = signature == previous ? unchanged + 1 : 0;
if (unchanged >= 2)
throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "The message was not found before the history viewport stopped changing.");
previous = signature;
if (page == maxScrolls) break;
ScrollNavigationList(main, WechatLocators.MessageList, WechatScrollDirection.Up);
await Task.Delay(350, cancellationToken).ConfigureAwait(false);
}
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The history search reached its scroll limit; absence was not established.");
}, cancellationToken);
private static async Task GoToLatestCoreAsync(AutomationElement main, string session, int maxScrolls, CancellationToken cancellationToken)
{
var previous = "";
var unchanged = 0;
for (var page = 0; page <= maxScrolls; page++)
{
cancellationToken.ThrowIfCancellationRequested();
RequireNavigationSession(main, session);
var list = FindByAutomationId(main, WechatLocators.MessageList)
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Message list was not found.");
if (list.Patterns.Scroll.IsSupported)
{
var pattern = list.Patterns.Scroll.Pattern;
ExecuteInputStep("latest-messages", () => pattern.SetScrollPercent(-1, 100));
await Task.Delay(350, cancellationToken).ConfigureAwait(false);
RequireNavigationSession(main, session);
var fresh = FindByAutomationId(main, WechatLocators.MessageList)!;
if (fresh.Patterns.Scroll.IsSupported && fresh.Patterns.Scroll.Pattern.VerticalScrollPercent.Value >= 99.9) return;
}
var signature = string.Join('|', ReadVisible(main).Select(message => message.Fingerprint));
unchanged = signature == previous ? unchanged + 1 : 0;
if (unchanged >= 2) return; // Bounded stable-viewport fallback; not a database history completeness claim.
previous = signature;
if (page == maxScrolls) break;
ScrollNavigationList(main, WechatLocators.MessageList, WechatScrollDirection.Down);
await Task.Delay(350, cancellationToken).ConfigureAwait(false);
}
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The latest-message navigation reached its scroll limit.");
}
public static Task<long> OpenMergedChatAsync(string session, string fingerprint, CancellationToken cancellationToken = default) =>
WithMainWindowAsync(async (main, automation) =>
{
await OpenNamedSessionAsync(main, session, cancellationToken).ConfigureAwait(false);
var window = await OpenMergedChatWindowCoreAsync(main, automation, session, fingerprint, cancellationToken).ConfigureAwait(false);
return window.Properties.NativeWindowHandle.ValueOrDefault.ToInt64();
}, cancellationToken);
private static async Task<AutomationElement> OpenMergedChatWindowCoreAsync(AutomationElement main, FlaUI.UIA3.UIA3Automation automation,
string session, string fingerprint, CancellationToken cancellationToken)
{
RequireNavigationSession(main, session);
var message = WechatNavigationRules.FindMessage(ReadVisible(main), fingerprint)
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "The merged-chat message is not currently loaded; locate it first.");
if (message.Type != ChatMessageType.Merge)
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "The selected message is not a merged-chat record.");
var before = automation.GetDesktop().FindAllChildren()
.Select(element => element.Properties.NativeWindowHandle.ValueOrDefault).ToHashSet();
var bubble = FindVisibleMessageElement(main, message.Fingerprint);
ExecuteInputStep("open-merged-chat", () => bubble.DoubleClick());
var started = System.Diagnostics.Stopwatch.StartNew();
while (started.Elapsed < TimeSpan.FromSeconds(8))
{
cancellationToken.ThrowIfCancellationRequested();
var windows = automation.GetDesktop().FindAllChildren()
.Where(element => element.Properties.ProcessId.ValueOrDefault == main.Properties.ProcessId.ValueOrDefault
&& !before.Contains(element.Properties.NativeWindowHandle.ValueOrDefault)
&& !element.Properties.IsOffscreen.ValueOrDefault && !element.BoundingRectangle.IsEmpty
&& SafeName(element).Contains("聊天记录", StringComparison.Ordinal)).ToArray();
if (windows.Length > 1)
throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Multiple new merged-chat windows appeared.");
if (windows.Length == 1) return windows[0];
await Task.Delay(200, cancellationToken).ConfigureAwait(false);
}
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "No unique new merged-chat window was confirmed.");
}
private static void RequireNavigationSession(AutomationElement main, string session)
{
if (!IsListeningSession(main, session, independent: false))
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The conversation changed during navigation.");
}
private static void ScrollNavigationList(AutomationElement main, string automationId, WechatScrollDirection direction)
{
var list = FindByAutomationId(main, automationId)
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "The navigation list was not found.");
ExecuteInputStep("scroll-navigation-list", () =>
{
if (list.Patterns.Scroll.IsSupported)
{
var pattern = list.Patterns.Scroll.Pattern;
if (!pattern.VerticallyScrollable.Value) return;
var step = Math.Max(1, pattern.VerticalViewSize.Value * 0.8);
var percent = Math.Clamp(pattern.VerticalScrollPercent.Value + (direction == WechatScrollDirection.Up ? -step : step), 0, 100);
pattern.SetScrollPercent(-1, percent);
return;
}
MoveToCenter(list);
Mouse.Scroll(direction == WechatScrollDirection.Up ? 5 : -5);
});
}
private static IReadOnlyList<WechatSessionSnapshot> ReadSessionViewport(AutomationElement main)
{
var list = FindByAutomationId(main, WechatLocators.SessionList)
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Session list was not found.");
var current = SafeName(FindByAutomationId(main, WechatLocators.CurrentChatName));
return WechatSessionParser.Parse(list.FindAllDescendants()
.Where(element => !element.Properties.IsOffscreen.ValueOrDefault && element.BoundingRectangle.IntersectsWith(list.BoundingRectangle))
.Select(element => (SafeAutomationId(element), SafeName(element))), current);
}
private static string SessionViewportKey(IReadOnlyList<WechatSessionSnapshot> sessions) =>
string.Join('|', sessions.Select(session => session.AutomationId));
}