187 lines
11 KiB
C#
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));
|
|
}
|