feat: send text to named sessions with draft protection and listener isolation
- chat send and listener-smoke accept --session; defaults remain File Transfer Assistant. - Reject existing drafts, reacquire the send control after input changes, and require a fresh message fingerprint for confirmation without retrying failed sends. - Test-NamedTextSend sends the same text twice and verifies distinct confirmed fingerprints; Test-NamedListeners -SendMarkers verifies per-window own events exactly once with zero cross-session events. Note export now requires a unique fresh owned note window and keeps repeated paragraphs. 97 Core tests pass; real-machine validated.
This commit is contained in:
@@ -122,9 +122,11 @@ public static partial class WechatChatClient
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<ChatMessageSnapshot> SendTextAsync(string text, CancellationToken cancellationToken)
|
||||
public static async Task<ChatMessageSnapshot> SendTextAsync(string text, CancellationToken cancellationToken,
|
||||
string session = WechatLocators.FileTransferAssistant)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(text);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(session);
|
||||
if (text.Length > 4000 || text.Contains('\r') || text.Contains('\n'))
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "M1 text messages must be one line and no longer than 4000 characters.");
|
||||
@@ -135,26 +137,31 @@ public static partial class WechatChatClient
|
||||
{
|
||||
using var automation = new UIA3Automation();
|
||||
var window = AttachWindow(automation, cancellationToken);
|
||||
await OpenFileTransferAssistantCoreAsync(window, 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);
|
||||
|
||||
var input = FindByAutomationId(window, WechatLocators.ChatInput)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, $"Control {WechatLocators.ChatInput} was not found.");
|
||||
var send = window.FindAllDescendants().FirstOrDefault(element =>
|
||||
SafeControlType(element) == ControlType.Button && SafeName(element) == WechatLocators.Send)
|
||||
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Send button 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 (!string.Equals(inputBox.Text, text, StringComparison.Ordinal))
|
||||
if (!IsListeningSession(window, session, independent: false) || !string.Equals(inputBox.Text, text, StringComparison.Ordinal))
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.ResultUnconfirmed, "The chat input did not contain the exact requested text; 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.");
|
||||
ExecuteInputStep("click-send", () => ClickCenter(send));
|
||||
|
||||
var confirmed = await WaitForMessageAsync(text, TimeSpan.FromSeconds(20), cancellationToken);
|
||||
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.");
|
||||
}
|
||||
finally
|
||||
@@ -667,6 +674,8 @@ public static partial class WechatChatClient
|
||||
|
||||
private static async Task<ChatMessageSnapshot?> WaitForMessageAsync(
|
||||
string text,
|
||||
string session,
|
||||
IReadOnlySet<string> baseline,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -676,9 +685,12 @@ public static partial class WechatChatClient
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using var freshAutomation = new UIA3Automation();
|
||||
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, text, StringComparison.Ordinal));
|
||||
: ReadVisible(window).LastOrDefault(message =>
|
||||
string.Equals(message.Text, text, StringComparison.Ordinal) && !baseline.Contains(message.Fingerprint));
|
||||
if (match is not null)
|
||||
{
|
||||
return match;
|
||||
@@ -833,9 +845,14 @@ public static partial class WechatChatClient
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (WxAgentException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, $"UI input failed at {step}.", exception);
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidOperationState,
|
||||
$"UI input failed at {step} ({exception.GetType().Name}, HRESULT 0x{exception.HResult:X8}).", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user