feat: generalize session sending with multiline text and structured mentions
Add named-session file/image/reply sending, preserve multiline text, and share safe member/all mention handling. Protect existing drafts, bind popup selection to the current window, validate structured mention tokens, and require fresh matching visible messages without automatic resend. Core tests: 121 passed. Windows 4.1.13.63 final validation: 9/9 passed. Record explicit coverage limits and sanitized evidence.
This commit is contained in:
@@ -58,20 +58,22 @@ public static class VisibleMessageParser
|
||||
|
||||
public static (string Text, QuotedMessageSnapshot? Quote) ParseQuote(string accessibleName)
|
||||
{
|
||||
var lines = accessibleName.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (lines.Length >= 2 && lines[^1].StartsWith("引用 ", StringComparison.Ordinal))
|
||||
var normalized = accessibleName.ReplaceLineEndings("\n");
|
||||
var lastLine = normalized.LastIndexOf('\n');
|
||||
if (lastLine >= 0 && normalized[(lastLine + 1)..].StartsWith("引用 ", StringComparison.Ordinal))
|
||||
{
|
||||
var descriptor = lines[^1];
|
||||
var descriptor = normalized[(lastLine + 1)..];
|
||||
var messageMarker = descriptor.IndexOf(" 的消息", StringComparison.Ordinal);
|
||||
var separator = descriptor.IndexOfAny([':', ':']);
|
||||
if (messageMarker > 3 && separator > messageMarker)
|
||||
{
|
||||
var sender = descriptor[3..messageMarker].Trim();
|
||||
var quoteText = descriptor[(separator + 1)..].Trim();
|
||||
return (string.Join('\n', lines[..^1]), new QuotedMessageSnapshot(sender, quoteText));
|
||||
return (normalized[..lastLine], new QuotedMessageSnapshot(sender, quoteText));
|
||||
}
|
||||
}
|
||||
|
||||
var lines = accessibleName.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var quoteIndex = Array.FindIndex(lines, line =>
|
||||
string.Equals(line, "引用", StringComparison.Ordinal) || line.StartsWith("引用:", StringComparison.Ordinal));
|
||||
if (quoteIndex < 0 || lines.Length - quoteIndex < 3)
|
||||
|
||||
@@ -23,6 +23,8 @@ public static class WechatLocators
|
||||
public const string ChatPage = "chat_message_page";
|
||||
public const string MessageList = "chat_message_list";
|
||||
public const string ChatInput = "chat_input_field";
|
||||
public const string MentionPopover = "MentionPopover";
|
||||
public const string MentionList = "chat_mention_list";
|
||||
public const string ToolBar = "tool_bar_accessible";
|
||||
public const string CurrentChatName = "content_view.top_content_view.title_h_view.left_v_view.left_content_v_view.left_ui_.big_title_line_h_view.current_chat_name_label";
|
||||
public const string ChatBubbleItem = "chat_message_list.qt_scrollarea_viewport.chat_bubble_item_view";
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public static class WechatSendConfirmation
|
||||
{
|
||||
public static bool IsMentionToken(string text) => text.TrimEnd() == "\uFFFC";
|
||||
|
||||
public static bool MatchesMention(ChatMessageSnapshot message, IReadOnlySet<string> baseline,
|
||||
string memberName, string? body)
|
||||
{
|
||||
if (baseline.Contains(message.Fingerprint) || message.Type != ChatMessageType.Text) return false;
|
||||
var expected = "@" + memberName + (string.IsNullOrEmpty(body) ? "" : " " + body);
|
||||
return string.Equals(NormalizeWhitespace(message.Text), NormalizeWhitespace(expected), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string NormalizeWhitespace(string text) =>
|
||||
string.Join(' ', text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
|
||||
|
||||
// UIA proves only a fresh matching visible row, not delivery or sender identity.
|
||||
public static bool Matches(ChatMessageSnapshot message, IReadOnlySet<string> baseline,
|
||||
ChatMessageType type, string? expectedText)
|
||||
{
|
||||
if (baseline.Contains(message.Fingerprint) || message.Type != type) return false;
|
||||
if (expectedText is null) return true;
|
||||
return type == ChatMessageType.File
|
||||
? message.Text.Split('\n').Any(line => string.Equals(line.TrimEnd('\r'), expectedText, StringComparison.Ordinal))
|
||||
: string.Equals(message.Text, expectedText, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public static class WechatTextInput
|
||||
{
|
||||
public static string Prepare(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Message text must not be empty.");
|
||||
var normalized = text.ReplaceLineEndings("\n");
|
||||
if (normalized.Length > 4000 || normalized.Any(character => char.IsControl(character) && character is not ('\n' or '\t')))
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Message text must not exceed 4000 characters or contain unsupported control characters.");
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user