Initial import

This commit is contained in:
2026-09-04 21:46:38 +08:00
commit 5e3aa97644
36 changed files with 4608 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
using System.Security.Cryptography;
using System.Text;
namespace WxAgent.Core;
public enum ChatMessageType
{
Text,
Image,
File,
Video,
Voice,
Link,
Quote,
System
}
public sealed record QuotedMessageSnapshot(string? Sender, string Text);
public sealed record ChatMessageSource(string Text, string? SourceId = null);
public sealed record ChatMessageSnapshot(
string Text,
string Fingerprint,
int VisibleIndex,
ChatMessageType Type,
QuotedMessageSnapshot? Quote = null);
public static class VisibleMessageParser
{
public static IReadOnlyList<ChatMessageSnapshot> Parse(IEnumerable<string> accessibleNames) =>
Parse(accessibleNames.Select(text => new ChatMessageSource(text)));
public static IReadOnlyList<ChatMessageSnapshot> Parse(IEnumerable<ChatMessageSource> sources)
{
var occurrences = new Dictionary<string, int>(StringComparer.Ordinal);
var messages = new List<ChatMessageSnapshot>();
foreach (var source in sources.Where(value => !string.IsNullOrWhiteSpace(value.Text)))
{
var text = source.Text;
var occurrence = occurrences.TryGetValue(text, out var count) ? count : 0;
occurrences[text] = occurrence + 1;
var identity = $"{occurrence}:{text}";
var fingerprint = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant();
var (replyText, quote) = ParseQuote(text);
var type = quote is null ? Classify(replyText) : ChatMessageType.Quote;
messages.Add(new ChatMessageSnapshot(replyText, fingerprint, messages.Count, type, quote));
}
return messages;
}
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 descriptor = lines[^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));
}
}
var quoteIndex = Array.FindIndex(lines, line =>
string.Equals(line, "引用", StringComparison.Ordinal) || line.StartsWith("引用:", StringComparison.Ordinal));
if (quoteIndex < 0 || lines.Length - quoteIndex < 3)
{
return (accessibleName, null);
}
var marker = lines[quoteIndex];
var legacySender = marker.Length > 3 ? marker[3..].Trim() : null;
var legacyQuoteText = string.Join('\n', lines[(quoteIndex + 1)..^1]);
return (lines[^1], new QuotedMessageSnapshot(string.IsNullOrWhiteSpace(legacySender) ? null : legacySender, legacyQuoteText));
}
public static ChatMessageType Classify(string text)
{
if (text is "[图片]" or "图片" || text.StartsWith("图片\n", StringComparison.Ordinal))
{
return ChatMessageType.Image;
}
if (text is "[视频]" or "视频" || text.StartsWith("视频\n", StringComparison.Ordinal))
{
return ChatMessageType.Video;
}
if (text is "[语音]" or "语音" || text.StartsWith("语音\n", StringComparison.Ordinal))
{
return ChatMessageType.Voice;
}
if (Uri.TryCreate(text, UriKind.Absolute, out var uri) && uri.Scheme is "http" or "https")
{
return ChatMessageType.Link;
}
if (text.StartsWith("文件\n", StringComparison.Ordinal))
{
return ChatMessageType.File;
}
if (!text.Contains(Path.DirectorySeparatorChar) && !text.Contains(Path.AltDirectorySeparatorChar) &&
Path.GetExtension(text) is { Length: > 1 } extension && extension.Length <= 10)
{
return ChatMessageType.File;
}
return text.StartsWith("[", StringComparison.Ordinal) && text.EndsWith("]", StringComparison.Ordinal)
? ChatMessageType.System
: ChatMessageType.Text;
}
}
public sealed class BoundedMessageDeduper(int capacity = 2048)
{
private readonly Queue<string> _order = new();
private readonly HashSet<string> _seen = new(StringComparer.Ordinal);
public bool TryAdd(string fingerprint)
{
ArgumentException.ThrowIfNullOrWhiteSpace(fingerprint);
if (!_seen.Add(fingerprint))
{
return false;
}
_order.Enqueue(fingerprint);
if (_order.Count > capacity)
{
_seen.Remove(_order.Dequeue());
}
return true;
}
}