feat: support long text and safe attachment batches
Build web service image / build (push) Successful in 1m15s
Build web service image / build (push) Successful in 1m15s
This commit is contained in:
@@ -112,7 +112,7 @@ public static partial class WechatChatClient
|
||||
public static async Task<ChatMessageSnapshot> SendTextAsync(string text, CancellationToken cancellationToken,
|
||||
string session = WechatLocators.FileTransferAssistant)
|
||||
{
|
||||
text = WechatTextInput.Prepare(text);
|
||||
var segments = WechatTextInput.SplitForSend(text);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(session);
|
||||
|
||||
await CommandQueue.WaitAsync(cancellationToken);
|
||||
@@ -122,30 +122,56 @@ public static partial class WechatChatClient
|
||||
var window = AttachWindow(automation, cancellationToken);
|
||||
await OpenNamedSessionAsync(window, session, cancellationToken).ConfigureAwait(false);
|
||||
await Task.Delay(500, cancellationToken);
|
||||
if (!IsListeningSession(window, session, independent: false))
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The requested conversation changed before input.");
|
||||
var baseline = ReadVisible(window).Select(message => message.Fingerprint).ToHashSet(StringComparer.Ordinal);
|
||||
ChatMessageSnapshot? lastConfirmed = null;
|
||||
|
||||
var input = FindByAutomationId(window, WechatLocators.ChatInput)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, $"Control {WechatLocators.ChatInput} was not found.");
|
||||
var inputBox = input.AsTextBox();
|
||||
if (!string.IsNullOrEmpty(inputBox.Text))
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "The conversation contains an existing draft; it was not overwritten.");
|
||||
ExecuteInputStep("set-input-value", () => inputBox.Text = text);
|
||||
await Task.Delay(150, cancellationToken);
|
||||
if (!IsListeningSession(window, session, independent: false) || !string.Equals(inputBox.Text.ReplaceLineEndings("\n"), text, StringComparison.Ordinal))
|
||||
for (var index = 0; index < segments.Count; index++)
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The chat input did not contain the exact requested text; send was aborted.");
|
||||
var sendAttempted = false;
|
||||
try
|
||||
{
|
||||
if (index > 0)
|
||||
window = AttachWindow(automation, cancellationToken);
|
||||
if (!IsListeningSession(window, session, independent: false))
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, $"The requested conversation changed before text segment {index + 1}/{segments.Count}.");
|
||||
|
||||
var segment = segments[index];
|
||||
var baseline = CountFingerprints(ReadVisible(window));
|
||||
var input = FindByAutomationId(window, WechatLocators.ChatInput)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, $"Control {WechatLocators.ChatInput} was not found.");
|
||||
var inputBox = input.AsTextBox();
|
||||
if (!string.IsNullOrEmpty(inputBox.Text))
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "The conversation contains an existing draft; it was not overwritten.");
|
||||
ExecuteInputStep("set-input-value", () => inputBox.Text = segment);
|
||||
await Task.Delay(150, cancellationToken);
|
||||
if (!IsListeningSession(window, session, independent: false) || !string.Equals(inputBox.Text.ReplaceLineEndings("\n"), segment, StringComparison.Ordinal))
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, $"Text segment {index + 1}/{segments.Count} did not appear exactly in the chat input; send was aborted.");
|
||||
|
||||
// Setting input text can recreate the send control; acquire it only after the input mutation.
|
||||
var send = window.FindAllDescendants().FirstOrDefault(element =>
|
||||
SafeControlType(element) == ControlType.Button && SafeName(element) == WechatLocators.Send)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Send button was not found after input.");
|
||||
sendAttempted = true;
|
||||
ExecuteInputStep("click-send", () => ClickCenter(send));
|
||||
|
||||
lastConfirmed = await WaitForMessageAsync(segment, session, baseline, TimeSpan.FromSeconds(20), cancellationToken);
|
||||
if (lastConfirmed is null)
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed,
|
||||
$"Text segment {index + 1}/{segments.Count} was not confirmed; earlier segments may already have been sent. Do not retry blindly.");
|
||||
}
|
||||
catch (Exception exception) when (sendAttempted && exception is not OutOfMemoryException)
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed,
|
||||
$"Text segment {index + 1}/{segments.Count} send result is uncertain; it may have been delivered and {index} earlier segment(s) were confirmed. Do not retry the whole text blindly.");
|
||||
}
|
||||
catch (Exception exception) when (index > 0 && exception is not OutOfMemoryException)
|
||||
{
|
||||
var cause = exception is WxAgentException agentException ? agentException.Code.ToString() : exception.GetType().Name;
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed,
|
||||
$"Text sending stopped after {index} confirmed segment(s) (cause {cause}); earlier text may have been delivered. Do not retry the whole text blindly.");
|
||||
}
|
||||
}
|
||||
|
||||
// Setting input text can recreate the send control; acquire it only after the input mutation.
|
||||
var send = window.FindAllDescendants().FirstOrDefault(element =>
|
||||
SafeControlType(element) == ControlType.Button && SafeName(element) == WechatLocators.Send)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Send button was not found after input.");
|
||||
ExecuteInputStep("click-send", () => ClickCenter(send));
|
||||
|
||||
var confirmed = await WaitForMessageAsync(text, session, baseline, TimeSpan.FromSeconds(20), cancellationToken);
|
||||
return confirmed ?? throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The text was entered but no matching visible message confirmed the send result.");
|
||||
return lastConfirmed!;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -169,7 +195,7 @@ public static partial class WechatChatClient
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The requested conversation changed before quoting.");
|
||||
}
|
||||
var before = ReadVisible(window).Select(message => message.Fingerprint).ToHashSet(StringComparer.Ordinal);
|
||||
var before = CountFingerprints(ReadVisible(window));
|
||||
var list = FindByAutomationId(window, WechatLocators.MessageList)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, $"Control {WechatLocators.MessageList} was not found.");
|
||||
var draft = FindByAutomationId(window, WechatLocators.ChatInput)
|
||||
@@ -229,6 +255,34 @@ public static partial class WechatChatClient
|
||||
public static Task<ChatMessageSnapshot> SendImageAsync(string path, CancellationToken cancellationToken, string session = WechatLocators.FileTransferAssistant) =>
|
||||
SendAttachmentAsync(path, asImage: true, session, cancellationToken);
|
||||
|
||||
public static async Task<IReadOnlyList<ChatMessageSnapshot>> SendFilesAsync(
|
||||
IReadOnlyList<string>? paths, CancellationToken cancellationToken,
|
||||
string session = WechatLocators.FileTransferAssistant)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(session);
|
||||
var fullPaths = WechatAttachmentInput.PrepareFilePaths(paths);
|
||||
var confirmed = new List<ChatMessageSnapshot>(fullPaths.Count);
|
||||
foreach (var fullPath in fullPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
confirmed.Add(await SendAttachmentAsync(fullPath, WechatAttachmentInput.IsImagePath(fullPath), session, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
catch (WxAgentException exception) when (confirmed.Count > 0)
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed,
|
||||
$"Attachment batch stopped after {confirmed.Count} confirmed item(s) (cause {exception.Code}); one or more attachments may already have been sent. Do not retry the whole batch blindly.");
|
||||
}
|
||||
catch (OperationCanceledException) when (confirmed.Count > 0)
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed,
|
||||
$"Attachment batch was cancelled after {confirmed.Count} confirmed item(s); one or more attachments may already have been sent. Do not retry the whole batch blindly.");
|
||||
}
|
||||
}
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
private static async Task<ChatMessageSnapshot> SendAttachmentAsync(string path, bool asImage, string session, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(session);
|
||||
@@ -249,7 +303,7 @@ public static partial class WechatChatClient
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The requested conversation changed before attachment input.");
|
||||
}
|
||||
var before = ReadVisible(window).Select(message => message.Fingerprint).ToHashSet(StringComparer.Ordinal);
|
||||
var before = CountFingerprints(ReadVisible(window));
|
||||
var input = FindByAutomationId(window, WechatLocators.ChatInput)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, $"Control {WechatLocators.ChatInput} was not found.");
|
||||
var inputText = input.AsTextBox().Text;
|
||||
@@ -275,11 +329,20 @@ public static partial class WechatChatClient
|
||||
var send = refreshedWindow.FindAllDescendants().FirstOrDefault(element =>
|
||||
SafeControlType(element) == ControlType.Button && SafeName(element) == WechatLocators.Send)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Send button was not found after attachment paste.");
|
||||
ClickCenter(send);
|
||||
var confirmed = await WaitForNewMessageAsync(before, session,
|
||||
asImage ? ChatMessageType.Image : ChatMessageType.File, asImage ? null : Path.GetFileName(fullPath),
|
||||
TimeSpan.FromSeconds(30), cancellationToken);
|
||||
return confirmed ?? throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The attachment selection completed but no matching new visible message confirmed the send result.");
|
||||
try
|
||||
{
|
||||
ClickCenter(send);
|
||||
var confirmed = await WaitForNewMessageAsync(before, session,
|
||||
asImage ? ChatMessageType.Image : ChatMessageType.File, asImage ? null : Path.GetFileName(fullPath),
|
||||
TimeSpan.FromSeconds(30), cancellationToken);
|
||||
return confirmed ?? throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed,
|
||||
"The attachment may have been sent, but no matching new visible message confirmed it. Do not retry blindly.");
|
||||
}
|
||||
catch (Exception exception) when (exception is not OutOfMemoryException)
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed,
|
||||
"The attachment send was attempted, but its result could not be confirmed. It may have been delivered; do not retry blindly.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -657,7 +720,7 @@ public static partial class WechatChatClient
|
||||
}
|
||||
|
||||
private static async Task<ChatMessageSnapshot?> WaitForNewMessageAsync(
|
||||
IReadOnlySet<string> previous,
|
||||
IReadOnlyDictionary<string, int> previousCounts,
|
||||
string session,
|
||||
ChatMessageType type,
|
||||
string? expectedText,
|
||||
@@ -673,10 +736,14 @@ public static partial class WechatChatClient
|
||||
var window = WechatDoctor.FindWechatWindow(automation);
|
||||
if (window is not null && !IsListeningSession(window, session, independent: false))
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The conversation changed while confirming the send; no retry was attempted.");
|
||||
var message = window is null ? null : ReadVisible(window).LastOrDefault(item =>
|
||||
IReadOnlyList<ChatMessageSnapshot> visible = window is null
|
||||
? Array.Empty<ChatMessageSnapshot>()
|
||||
: ReadVisible(window);
|
||||
var currentCounts = CountFingerprints(visible);
|
||||
var message = visible.LastOrDefault(item =>
|
||||
mentionMember is null
|
||||
? WechatSendConfirmation.Matches(item, previous, type, expectedText)
|
||||
: WechatSendConfirmation.MatchesMention(item, previous, mentionMember, expectedText));
|
||||
? WechatSendConfirmation.Matches(item, previousCounts, currentCounts, type, expectedText)
|
||||
: WechatSendConfirmation.MatchesMention(item, previousCounts, currentCounts, mentionMember, expectedText));
|
||||
if (message is not null) return message;
|
||||
await Task.Delay(250, cancellationToken);
|
||||
}
|
||||
@@ -687,7 +754,7 @@ public static partial class WechatChatClient
|
||||
private static async Task<ChatMessageSnapshot?> WaitForMessageAsync(
|
||||
string text,
|
||||
string session,
|
||||
IReadOnlySet<string> baseline,
|
||||
IReadOnlyDictionary<string, int> baseline,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -699,10 +766,13 @@ public static partial class WechatChatClient
|
||||
var window = WechatDoctor.FindWechatWindow(freshAutomation);
|
||||
if (window is not null && !IsListeningSession(window, session, independent: false))
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The conversation changed while confirming the send; no retry was attempted.");
|
||||
var match = window is null
|
||||
? null
|
||||
: ReadVisible(window).LastOrDefault(message =>
|
||||
string.Equals(message.Text.ReplaceLineEndings("\n"), text, StringComparison.Ordinal) && !baseline.Contains(message.Fingerprint));
|
||||
IReadOnlyList<ChatMessageSnapshot> visible = window is null
|
||||
? Array.Empty<ChatMessageSnapshot>()
|
||||
: ReadVisible(window);
|
||||
var visibleCounts = CountFingerprints(visible);
|
||||
var match = visible.LastOrDefault(message =>
|
||||
string.Equals(message.Text.ReplaceLineEndings("\n"), text, StringComparison.Ordinal)
|
||||
&& WechatSendConfirmation.IsNewOccurrence(message.Fingerprint, baseline, visibleCounts));
|
||||
if (match is not null)
|
||||
{
|
||||
return match;
|
||||
@@ -714,6 +784,10 @@ public static partial class WechatChatClient
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> CountFingerprints(IEnumerable<ChatMessageSnapshot> messages) =>
|
||||
messages.GroupBy(message => message.Fingerprint, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal);
|
||||
|
||||
private static IReadOnlyList<ChatMessageSnapshot> ReadVisible(AutomationElement window)
|
||||
{
|
||||
var list = FindByAutomationId(window, WechatLocators.MessageList)
|
||||
|
||||
Reference in New Issue
Block a user